pi-cursor-sdk 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/cursor-model-ux-spec.md +1 -1
  3. package/docs/cursor-native-tool-replay.md +9 -9
  4. package/package.json +1 -1
  5. package/scripts/platform-smoke/card-detect.mjs +1 -1
  6. package/src/context-window-cache.ts +10 -14
  7. package/src/context.ts +1 -1
  8. package/src/cursor-agent-message-web-tools.ts +2 -1
  9. package/src/cursor-agents-context-registration.ts +18 -0
  10. package/src/cursor-agents-context.ts +21 -30
  11. package/src/cursor-edit-diff.ts +4 -2
  12. package/src/cursor-fallback-warning.ts +22 -0
  13. package/src/cursor-incomplete-tool-visibility.ts +5 -11
  14. package/src/cursor-live-run-coordinator.ts +1 -1
  15. package/src/cursor-mcp-timeout-override.ts +0 -2
  16. package/src/cursor-model-lifecycle.ts +72 -0
  17. package/src/cursor-native-replay-routing.ts +1 -1
  18. package/src/cursor-native-replay-trace.ts +1 -1
  19. package/src/cursor-native-tool-display-registration.ts +16 -28
  20. package/src/cursor-native-tool-display-replay.ts +12 -47
  21. package/src/cursor-native-tool-display-state.ts +1 -1
  22. package/src/cursor-native-tool-display-tools.ts +10 -18
  23. package/src/cursor-native-tool-names.ts +16 -0
  24. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  25. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  26. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  27. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  28. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  29. package/src/cursor-pi-tool-bridge.ts +7 -7
  30. package/src/cursor-provider-lazy.ts +51 -0
  31. package/src/cursor-provider-live-run-drain.ts +1 -1
  32. package/src/cursor-provider-run-finalizer.ts +5 -5
  33. package/src/cursor-provider-run-outcome.ts +0 -1
  34. package/src/cursor-provider-turn-coordinator.ts +4 -5
  35. package/src/cursor-provider-turn-display-router.ts +5 -1
  36. package/src/cursor-provider-turn-emit.ts +1 -1
  37. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  38. package/src/cursor-provider-turn-prepare.ts +13 -9
  39. package/src/cursor-provider-turn-runner.ts +3 -11
  40. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  41. package/src/cursor-provider-turn-send.ts +7 -2
  42. package/src/cursor-provider-turn-types.ts +1 -3
  43. package/src/cursor-provider.ts +3 -2
  44. package/src/cursor-question-tool.ts +5 -18
  45. package/src/cursor-record-utils.ts +42 -0
  46. package/src/cursor-replay-activity-builders.ts +16 -122
  47. package/src/cursor-replay-tool-details.ts +57 -197
  48. package/src/cursor-sdk-event-debug.ts +6 -6
  49. package/src/cursor-sensitive-text.ts +4 -4
  50. package/src/cursor-session-agent-lifecycle.ts +47 -0
  51. package/src/cursor-session-agent.ts +9 -47
  52. package/src/cursor-session-scope.ts +23 -4
  53. package/src/cursor-setting-sources.ts +8 -8
  54. package/src/cursor-skill-tool.ts +25 -32
  55. package/src/cursor-state.ts +66 -45
  56. package/src/cursor-tool-lifecycle.ts +16 -9
  57. package/src/cursor-tool-presentation-registry.ts +42 -169
  58. package/src/cursor-tool-result-display-readers.ts +185 -0
  59. package/src/cursor-tool-transcript.ts +17 -33
  60. package/src/cursor-tool-visibility.ts +9 -1
  61. package/src/cursor-transcript-tool-formatters.ts +23 -172
  62. package/src/cursor-transcript-tool-specs.ts +17 -57
  63. package/src/cursor-transcript-utils.ts +2 -34
  64. package/src/cursor-usage-accounting.ts +0 -6
  65. package/src/cursor-web-tool-activity.ts +4 -12
  66. package/src/cursor-web-tool-args.ts +1 -9
  67. package/src/index.ts +15 -16
  68. package/src/model-discovery.ts +5 -4
  69. package/src/model-list-cache.ts +37 -38
  70. package/src/cursor-native-tool-display.ts +0 -10
  71. package/src/cursor-provider-turn-api-key.ts +0 -1
  72. package/src/cursor-provider-turn-message-offset.ts +0 -15
  73. package/src/cursor-session-cwd.ts +0 -28
  74. package/src/cursor-tool-names.ts +0 -18
@@ -1,6 +1,11 @@
1
+ import { asRecord, getString } from "./cursor-record-utils.js";
2
+ import { normalizeCursorToolName } from "./cursor-tool-presentation-registry.js";
3
+ import {
4
+ buildCursorPiToolDisplayFromSpec,
5
+ formatCursorToolTranscriptFromSpec,
6
+ type ToolDisplayContext,
7
+ } from "./cursor-transcript-tool-specs.js";
1
8
  import {
2
- asRecord,
3
- getString,
4
9
  getToolArgs,
5
10
  getToolName,
6
11
  getToolResult,
@@ -8,27 +13,12 @@ import {
8
13
  type CursorPiToolDisplay,
9
14
  type TranscriptOptions,
10
15
  } from "./cursor-transcript-utils.js";
11
- import { normalizeCursorToolName as normalizeToolName } from "./cursor-tool-presentation-registry.js";
12
- import {
13
- buildCursorPiToolDisplayFromSpec,
14
- formatCursorToolTranscriptFromSpec,
15
- type ToolDisplayContext,
16
- } from "./cursor-transcript-tool-specs.js";
17
16
  import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
18
17
 
19
18
  export type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
19
+ export type { ToolDisplayContext } from "./cursor-transcript-tool-specs.js";
20
20
 
21
- export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
22
- const name = normalizeToolName(getToolName(toolCall));
23
- if (name !== "createPlan") return undefined;
24
- const args = getToolArgs(toolCall);
25
- const result = normalizeResult(getToolResult(toolCall));
26
- const plan = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
27
- const trimmed = plan?.trim();
28
- return trimmed || undefined;
29
- }
30
-
31
- function buildToolDisplayContext(toolCall: unknown, options: TranscriptOptions): ToolDisplayContext {
21
+ function buildToolDisplayContext(toolCall: unknown, options: TranscriptOptions = {}): ToolDisplayContext {
32
22
  const rawName = getToolName(toolCall);
33
23
  const args = getToolArgs(toolCall);
34
24
  return {
@@ -48,18 +38,12 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
48
38
  return buildCursorPiToolDisplayFromSpec(buildToolDisplayContext(toolCall, options));
49
39
  }
50
40
 
51
- export function mergeCursorToolCalls(startedToolCall: unknown, completedToolCall: unknown): unknown {
52
- const started = asRecord(startedToolCall);
53
- const completed = asRecord(completedToolCall);
54
- if (!started) return completedToolCall;
55
- if (!completed) return startedToolCall;
56
- return {
57
- ...started,
58
- ...completed,
59
- name: completed.name ?? started.name,
60
- type: completed.type ?? started.type,
61
- args: completed.args ?? started.args,
62
- input: completed.input ?? started.input,
63
- result: completed.result ?? started.result,
64
- };
41
+ export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
42
+ const name = normalizeCursorToolName(getToolName(toolCall));
43
+ if (name !== "createPlan") return undefined;
44
+ const args = getToolArgs(toolCall);
45
+ const result = normalizeResult(getToolResult(toolCall));
46
+ const plan = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
47
+ const trimmed = plan?.trim();
48
+ return trimmed || undefined;
65
49
  }
@@ -1,4 +1,8 @@
1
- import { getCursorReplayActivityTitle, getCursorToolVisibilityPolicy, normalizeCursorToolName as normalizeToolName } from "./cursor-tool-presentation-registry.js";
1
+ import {
2
+ getCursorReplayActivityTitle,
3
+ getCursorToolVisibilityPolicy,
4
+ normalizeCursorToolName as normalizeToolName,
5
+ } from "./cursor-tool-presentation-registry.js";
2
6
  import { getToolArgs, getToolName } from "./cursor-transcript-utils.js";
3
7
  import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
4
8
 
@@ -14,6 +18,10 @@ export interface CursorToolVisibility {
14
18
  fastLocalDiscovery: boolean;
15
19
  }
16
20
 
21
+ export function getNormalizedCursorToolName(toolCall: unknown): string {
22
+ return classifyCursorToolVisibility(toolCall).normalizedName;
23
+ }
24
+
17
25
  export function classifyCursorToolVisibility(toolCall: unknown): CursorToolVisibility {
18
26
  const args = getToolArgs(toolCall);
19
27
  const displayName = resolveTranscriptToolName(getToolName(toolCall), args);
@@ -1,19 +1,29 @@
1
1
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
2
- import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
- import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
4
- import { getFirstStringByKeys } from "./cursor-record-utils.js";
2
+ import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
5
3
  import {
6
4
  asRecord,
7
- formatDisplayPath,
8
- formatDiffHeaderLine,
9
- formatDiffString,
10
- formatError,
11
- formatPathArg,
12
5
  getArray,
13
6
  getBoolean,
7
+ getFirstStringByKeys,
14
8
  getNumber,
15
9
  getRecord,
16
10
  getString,
11
+ } from "./cursor-record-utils.js";
12
+ import {
13
+ collectTaskText,
14
+ getGenerateImageDisplayPath,
15
+ readMcpDisplayResult,
16
+ getReadLintDiagnostics,
17
+ getReadLintPaths,
18
+ getTaskDescription,
19
+ getTodoItems,
20
+ } from "./cursor-tool-result-display-readers.js";
21
+
22
+ import {
23
+ formatDisplayPath,
24
+ formatDiffString,
25
+ formatError,
26
+ formatPathArg,
17
27
  joinSections,
18
28
  limitItems,
19
29
  limitText,
@@ -23,7 +33,6 @@ import {
23
33
  DEFAULT_NATIVE_READ_DISPLAY_LINES,
24
34
  readFilePreview,
25
35
  stringifyUnknown,
26
- firstNonEmptyLine,
27
36
  truncateArg,
28
37
  type NormalizedResult,
29
38
  type TranscriptOptions,
@@ -445,38 +454,6 @@ export function formatDelete(args: Record<string, unknown>, result: NormalizedRe
445
454
  return joinSections(`delete ${path}`, fileSize !== undefined ? `Deleted ${fileSize} bytes` : stringifyUnknown(result.value));
446
455
  }
447
456
 
448
- export function getReadLintPaths(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string[] {
449
- const explicitPaths = Array.isArray(args.paths)
450
- ? args.paths.filter((entry): entry is string => typeof entry === "string")
451
- : typeof args.path === "string"
452
- ? [args.path]
453
- : [];
454
- const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
455
- .map((file) => getString(asRecord(file), "path"))
456
- .filter((entry): entry is string => Boolean(entry));
457
- return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
458
- }
459
-
460
- export function getReadLintDiagnostics(result: NormalizedResult, options: TranscriptOptions): string[] {
461
- const value = asRecord(result.value);
462
- const files = getArray(value, "fileDiagnostics") ?? [];
463
- const lines: string[] = [];
464
- for (const file of files) {
465
- const fileRecord = asRecord(file);
466
- const pathValue = getString(fileRecord, "path");
467
- const path = pathValue ? formatDisplayPath(pathValue, options.cwd) : "unknown";
468
- const diagnostics = getArray(fileRecord, "diagnostics") ?? [];
469
- for (const diagnostic of diagnostics) {
470
- const diagnosticRecord = asRecord(diagnostic);
471
- const severity = getString(diagnosticRecord, "severity") ?? "diagnostic";
472
- const message = getString(diagnosticRecord, "message") ?? "";
473
- const source = getString(diagnosticRecord, "source");
474
- lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
475
- }
476
- }
477
- return lines;
478
- }
479
-
480
457
  export function formatReadLints(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
481
458
  const paths = getReadLintPaths(args, result, options);
482
459
  const header = `readLints${paths.length > 0 ? ` ${paths.join(" ")}` : ""}`;
@@ -487,24 +464,6 @@ export function formatReadLints(args: Record<string, unknown>, result: Normalize
487
464
  return joinSections(header, limitText(lines.join("\n") || stringifyUnknown(result.value), options));
488
465
  }
489
466
 
490
- export function getTodoItems(args: Record<string, unknown>, result: NormalizedResult): Array<{ content: string; status?: string }> {
491
- const value = asRecord(result.value);
492
- const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
493
- const todos: Array<{ content: string; status?: string }> = [];
494
- for (const todo of rawTodos) {
495
- const record = asRecord(todo);
496
- const content = getString(record, "content");
497
- if (!content) continue;
498
- const status = getString(record, "status");
499
- todos.push(status ? { content, status } : { content });
500
- }
501
- return todos;
502
- }
503
-
504
- export function getTodoTotalCount(args: Record<string, unknown>, result: NormalizedResult, todos: Array<{ content: string; status?: string }>): number {
505
- return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
506
- }
507
-
508
467
  function formatTodoStatus(status: string | undefined): string {
509
468
  if (status === "completed") return "✓";
510
469
  if (status === "inProgress") return "…";
@@ -527,32 +486,6 @@ export function formatPlan(args: Record<string, unknown>, result: NormalizedResu
527
486
  return formatTodos(args, result, options, "createPlan");
528
487
  }
529
488
 
530
- export function getTaskDescription(args: Record<string, unknown>, result: NormalizedResult): string {
531
- return getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
532
- }
533
-
534
- function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: string[]): Record<string, unknown> | undefined {
535
- let current = record;
536
- for (const key of keys) {
537
- current = getRecord(current, key);
538
- if (!current) return undefined;
539
- }
540
- return current;
541
- }
542
-
543
- export function collectTaskText(result: NormalizedResult): string {
544
- const value = asRecord(result.value);
545
- const success = getNestedRecord(value, "result", "success");
546
- const command = getString(success, "command");
547
- const stdout = getString(success, "stdout");
548
- const interleavedOutput = getString(success, "interleavedOutput");
549
- const assistantMessages = (getArray(value, "conversationSteps") ?? [])
550
- .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
551
- .filter((entry): entry is string => Boolean(entry));
552
- const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
553
- return parts.join("\n");
554
- }
555
-
556
489
  export function formatTask(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
557
490
  const description = getTaskDescription(args, result);
558
491
  if (result.status === "error") return joinSections(`task ${description}`, formatError(result.error));
@@ -560,34 +493,10 @@ export function formatTask(args: Record<string, unknown>, result: NormalizedResu
560
493
  return joinSections(`task ${description}`, limitText(taskText || stringifyUnknown(result.value), options));
561
494
  }
562
495
 
563
- function getGenerateImageValue(result: NormalizedResult): Record<string, unknown> | undefined {
564
- return asRecord(result.value);
565
- }
566
-
567
- export function getGenerateImagePath(args: Record<string, unknown>, result: NormalizedResult): string | undefined {
568
- const value = getGenerateImageValue(result);
569
- return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
570
- }
571
-
572
- export function getGenerateImageDisplayPath(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string | undefined {
573
- const path = getGenerateImagePath(args, result);
574
- return path ? formatDisplayPath(path, options.cwd) : undefined;
575
- }
576
-
577
- export function inferImageMimeType(path: string | undefined): string | undefined {
578
- const lower = path?.toLowerCase();
579
- if (!lower) return undefined;
580
- if (lower.endsWith(".png")) return "image/png";
581
- if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
582
- if (lower.endsWith(".gif")) return "image/gif";
583
- if (lower.endsWith(".webp")) return "image/webp";
584
- return undefined;
585
- }
586
-
587
496
  export function formatGenerateImage(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
588
497
  const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
589
498
  if (result.status === "error") return joinSections(`generateImage ${prompt}`, formatError(result.error));
590
- const value = getGenerateImageValue(result);
499
+ const value = asRecord(result.value);
591
500
  const displayPath = getGenerateImageDisplayPath(args, result, options);
592
501
  const hasImageData = typeof value?.imageData === "string" && value.imageData.length > 0;
593
502
  const lines = [displayPath ? `Saved image: ${displayPath}` : undefined, hasImageData ? "Image data returned by Cursor SDK." : undefined].filter(
@@ -597,26 +506,6 @@ export function formatGenerateImage(args: Record<string, unknown>, result: Norma
597
506
  return joinSections(`generateImage ${prompt}`, limitText(stringifyUnknown(result.value), options));
598
507
  }
599
508
 
600
- function getMcpContentText(entry: unknown): string | undefined {
601
- const record = asRecord(entry);
602
- const directText = getString(record, "text");
603
- if (directText) return directText;
604
- const nestedText = getRecord(record, "text");
605
- return getString(nestedText, "text");
606
- }
607
-
608
- function describeNonTextMcpContent(entry: unknown): string {
609
- const record = asRecord(entry);
610
- const type = getString(record, "type") ?? "content";
611
- if (type === "image") {
612
- const mimeType = getString(record, "mimeType") ?? getString(record, "mime") ?? getString(record, "mediaType");
613
- return `[image${mimeType ? ` ${mimeType}` : ""} omitted]`;
614
- }
615
- if (type === "audio") return "[audio omitted]";
616
- if (type === "resource") return "[resource omitted]";
617
- return `[${type} omitted]`;
618
- }
619
-
620
509
  export function formatSemSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
621
510
  const query = getString(args, "query") ?? "semantic search";
622
511
  const header = `semSearch ${truncateArg(query)}`;
@@ -681,69 +570,31 @@ export function formatRecordScreen(args: Record<string, unknown>, result: Normal
681
570
  return joinSections(header, lines.join("\n"));
682
571
  }
683
572
 
684
- export function getMcpResultPreview(result: NormalizedResult): string | undefined {
685
- if (result.status === "error") return undefined;
686
- const value = asRecord(result.value);
687
- const content = getArray(value, "content") ?? [];
688
- for (const entry of content) {
689
- const text = getMcpContentText(entry);
690
- if (text) {
691
- const line = firstNonEmptyLine(text);
692
- if (line) return truncateArg(scrubSensitiveText(line), 120);
693
- }
694
- const summary = describeNonTextMcpContent(entry);
695
- if (summary) return summary;
696
- }
697
- return undefined;
698
- }
699
-
700
573
  function formatWebToolBody(
701
574
  toolLabel: string,
702
- args: Record<string, unknown>,
703
575
  result: NormalizedResult,
704
576
  options: TranscriptOptions,
705
- summaryArg: string | undefined,
706
577
  ): string {
707
578
  if (result.status === "error") return joinSections(toolLabel, formatError(result.error));
708
- const summary = summaryArg ? `${summaryArg}\n\n` : "";
709
- const value = asRecord(result.value);
710
- const isError = getBoolean(value, "isError");
711
- const content = getArray(value, "content") ?? [];
712
- const text = content
713
- .map((entry) => getMcpContentText(entry))
714
- .filter((entry): entry is string => Boolean(entry))
715
- .join("\n");
716
- const contentSummary = content.length > 0 ? content.map(describeNonTextMcpContent).join("\n") : stringifyUnknown(result.value);
717
- const body = `${isError ? "[tool error]\n" : ""}${text || contentSummary}`;
718
- return joinSections(toolLabel, limitText(`${summary}${body}`.trim(), options));
579
+ return joinSections(toolLabel, limitText(readMcpDisplayResult(result).body, options));
719
580
  }
720
581
 
721
582
  export function formatWebSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
722
583
  const query = extractWebSearchQuery(args);
723
584
  const header = query ? `web search ${query}` : "web search";
724
- return formatWebToolBody(header, args, result, options, undefined);
585
+ return formatWebToolBody(header, result, options);
725
586
  }
726
587
 
727
588
  export function formatWebFetch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
728
589
  const target = extractWebFetchTarget(args);
729
590
  const header = target ? `web fetch ${target}` : "web fetch";
730
- return formatWebToolBody(header, args, result, options, undefined);
591
+ return formatWebToolBody(header, result, options);
731
592
  }
732
593
 
733
594
  export function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
734
595
  const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
735
596
  if (result.status === "error") return joinSections(toolName, formatError(result.error));
736
-
737
- const value = asRecord(result.value);
738
- const isError = getBoolean(value, "isError");
739
- const content = getArray(value, "content") ?? [];
740
- const text = content
741
- .map((entry) => getMcpContentText(entry))
742
- .filter((entry): entry is string => Boolean(entry))
743
- .join("\n");
744
- const contentSummary = content.length > 0 ? content.map(describeNonTextMcpContent).join("\n") : stringifyUnknown(result.value);
745
- const body = `${isError ? "[tool error]\n" : ""}${text || contentSummary}`;
746
- return joinSections(toolName, limitText(body, options));
597
+ return joinSections(toolName, limitText(readMcpDisplayResult(result).body, options));
747
598
  }
748
599
 
749
600
  const UNKNOWN_TOOL_FALLBACK_MAX_ARGS = 8;
@@ -1,35 +1,27 @@
1
1
  import {
2
2
  CURSOR_KNOWN_NORMALIZED_TOOL_NAMES,
3
3
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
- getCursorReplayActivityLabelKey,
5
- getCursorReplayActivityTitle,
6
4
  getCursorReplayCallSummary,
7
- getCursorReplayDisplayLabel,
5
+ getCursorToolActivityTitle,
8
6
  getCursorToolActivityReplaySpec,
9
7
  getCursorToolGenerateImageReplaySpec,
10
8
  type CursorNormalizedToolName,
11
9
  type CursorReplayActivityToolName,
12
- type CursorReplayLegacyToolName,
13
10
  } from "./cursor-tool-presentation-registry.js";
14
11
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
15
12
  import {
16
13
  assembleCursorReplayActivityDetails,
17
14
  assembleCursorReplayGenerateImageDetails,
18
- buildCursorReplayNativeEditDetails,
19
- buildCursorReplayNativeWriteDetails,
20
15
  CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME,
21
16
  type CursorReplayToolDetails,
22
17
  } from "./cursor-replay-tool-details.js";
18
+ import { asRecord, getNumber, getString } from "./cursor-record-utils.js";
23
19
  import {
24
- asRecord,
25
20
  firstNonEmptyLine,
26
21
  formatDisplayPath,
27
22
  formatDiffString,
28
23
  formatError,
29
- getNumber,
30
- getString,
31
24
  limitText,
32
- stringifyUnknown,
33
25
  truncateArg,
34
26
  type CursorPiToolDisplay,
35
27
  type NormalizedResult,
@@ -71,8 +63,6 @@ import {
71
63
  getShellOutput,
72
64
  usesLocalReadPreview,
73
65
  } from "./cursor-transcript-tool-formatters.js";
74
- import type { CursorReplaySummaryArgs } from "./cursor-replay-summary-args.js";
75
-
76
66
  export interface ToolDisplayContext {
77
67
  rawName: string;
78
68
  name: string;
@@ -88,12 +78,6 @@ interface ToolDisplaySpec {
88
78
  buildPiToolDisplay: (context: ToolDisplayContext) => CursorPiToolDisplay;
89
79
  }
90
80
 
91
- function requireReplayActivityLabelKey(normalizedName: CursorReplayActivityToolName): CursorReplayLegacyToolName {
92
- const labelKey = getCursorReplayActivityLabelKey(normalizedName);
93
- if (!labelKey) throw new Error(`Missing replay activity label for ${normalizedName}`);
94
- return labelKey;
95
- }
96
-
97
81
  function textToolResult(text: string, details?: unknown): PiToolDisplayResult {
98
82
  return { content: [{ type: "text", text }], details };
99
83
  }
@@ -111,13 +95,6 @@ function buildCursorActivityDisplayArgs(
111
95
  };
112
96
  }
113
97
 
114
- function buildRegistryReplaySummary(
115
- labelKey: CursorReplayLegacyToolName,
116
- args: CursorReplaySummaryArgs,
117
- ): string | undefined {
118
- return getCursorReplayCallSummary(labelKey, args);
119
- }
120
-
121
98
  function buildReplaySummaryDisplay(
122
99
  toolName: string,
123
100
  args: Record<string, unknown>,
@@ -140,21 +117,16 @@ function buildReplaySummaryDisplay(
140
117
  };
141
118
  }
142
119
 
143
- function getCursorToolActivityTitle(toolName: string): string {
144
- return getCursorReplayActivityTitle(toolName) ?? buildGenericUnknownToolActivityTitle(toolName);
145
- }
146
-
147
120
  function buildActivityReplayDisplay(
148
121
  sourceToolName: NeutralActivityReplayToolName,
149
122
  context: ToolDisplayContext,
150
123
  ): CursorPiToolDisplay {
151
- const spec = TOOL_DISPLAY_SPECS[sourceToolName];
124
+ const spec = TOOL_DISPLAY_IMPLEMENTATIONS[sourceToolName];
152
125
  const activity = getCursorToolActivityReplaySpec(sourceToolName);
153
126
  if (!activity) throw new Error(`Missing activity replay spec for ${sourceToolName}`);
154
- const labelKey = requireReplayActivityLabelKey(sourceToolName);
155
- const activityTitle = getCursorReplayDisplayLabel(labelKey);
127
+ const activityTitle = getCursorToolActivityTitle(sourceToolName);
156
128
  const replayArgs = activity.buildActivityArgs(context);
157
- const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
129
+ const activitySummary = getCursorReplayCallSummary(sourceToolName, replayArgs);
158
130
  const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
159
131
  const contentText = spec.formatTranscript(context).trimEnd();
160
132
  const activityFields = activity.buildDetails(context, contentText);
@@ -170,13 +142,12 @@ function buildActivityReplayDisplay(
170
142
  }
171
143
 
172
144
  function buildGenerateImageReplayDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
173
- const spec = TOOL_DISPLAY_SPECS.generateImage;
145
+ const spec = TOOL_DISPLAY_IMPLEMENTATIONS.generateImage;
174
146
  const replay = getCursorToolGenerateImageReplaySpec("generateImage");
175
147
  if (!replay) throw new Error("Missing generate image replay spec");
176
- const labelKey = requireReplayActivityLabelKey("generateImage");
177
- const activityTitle = getCursorReplayDisplayLabel(labelKey);
148
+ const activityTitle = getCursorToolActivityTitle("generateImage");
178
149
  const replayArgs = replay.buildActivityArgs(context);
179
- const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
150
+ const activitySummary = getCursorReplayCallSummary("generateImage", replayArgs);
180
151
  const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
181
152
  const contentText = spec.formatTranscript(context).trimEnd();
182
153
  const details = assembleCursorReplayGenerateImageDetails(
@@ -188,24 +159,15 @@ function buildGenerateImageReplayDisplay(context: ToolDisplayContext): CursorPiT
188
159
  return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, context.result, contentText, details);
189
160
  }
190
161
 
191
- function buildGenericUnknownToolActivityTitle(displayName: string): string {
192
- if (displayName === "unknown") return "Cursor tool";
193
- return `Cursor ${truncateArg(displayName)}`;
194
- }
195
-
196
162
  function buildGenericPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
197
163
  const { rawName, name, args, result, options } = context;
198
164
  const displayName = rawName.trim() || name;
199
- const activityTitle = buildGenericUnknownToolActivityTitle(displayName);
165
+ const activityTitle = getCursorToolActivityTitle(displayName);
200
166
  const contentText = formatFallback(name, args, result, options);
201
167
  const fallbackBody = contentText.includes("\n\n") ? contentText.slice(contentText.indexOf("\n\n") + 2) : "";
202
168
  const activitySummary =
203
169
  result.status === "error" ? undefined : firstNonEmptyLine(fallbackBody);
204
- const activityArgs = buildCursorActivityDisplayArgs(
205
- { cursorToolName: displayName === "unknown" ? "tool" : displayName },
206
- activityTitle,
207
- activitySummary,
208
- );
170
+ const activityArgs = buildCursorActivityDisplayArgs({}, activityTitle, activitySummary);
209
171
  const summary =
210
172
  result.status === "error"
211
173
  ? undefined
@@ -252,14 +214,15 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
252
214
  const activityTitle = getCursorToolActivityTitle("edit");
253
215
  const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
254
216
  const contentText = formatEdit(activityArgs, result, options);
255
- const details = buildCursorReplayNativeEditDetails({
217
+ const details: CursorReplayToolDetails = {
218
+ variant: "nativeEdit",
256
219
  path: displayPath,
257
220
  linesAdded: getNumber(value, "linesAdded"),
258
221
  linesRemoved: getNumber(value, "linesRemoved"),
259
222
  diffString: normalizedDiff,
260
223
  diff: normalizedDiff,
261
224
  firstChangedLine: getNumber(value, "firstChangedLine"),
262
- });
225
+ };
263
226
  if (nativeEditArgs) {
264
227
  return {
265
228
  toolName: "edit",
@@ -299,13 +262,14 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
299
262
  const displayArgs = buildWriteDisplayArgs(args, options);
300
263
  const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
301
264
  const contentText = formatWrite(args, result, options).trimEnd();
302
- const details = buildCursorReplayNativeWriteDetails({
265
+ const details: CursorReplayToolDetails = {
266
+ variant: "nativeWrite",
303
267
  path: displayPath,
304
268
  linesCreated: getNumber(value, "linesCreated"),
305
269
  fileSize: getNumber(value, "fileSize"),
306
270
  fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
307
271
  expandedText: contentText,
308
- });
272
+ };
309
273
  if (content === undefined) {
310
274
  const activityTitle = getCursorToolActivityTitle("write");
311
275
  return buildReplaySummaryDisplay(
@@ -455,12 +419,8 @@ const TOOL_DISPLAY_IMPLEMENTATIONS: Record<CursorNormalizedToolName, ToolDisplay
455
419
 
456
420
  export const CURSOR_TOOL_DISPLAY_SPEC_KEYS = CURSOR_KNOWN_NORMALIZED_TOOL_NAMES;
457
421
 
458
- const TOOL_DISPLAY_SPECS = Object.fromEntries(
459
- CURSOR_KNOWN_NORMALIZED_TOOL_NAMES.map((name) => [name, TOOL_DISPLAY_IMPLEMENTATIONS[name]]),
460
- ) as Record<CursorNormalizedToolName, ToolDisplaySpec>;
461
-
462
422
  function getToolDisplaySpec(name: string): ToolDisplaySpec | undefined {
463
- if (Object.hasOwn(TOOL_DISPLAY_SPECS, name)) return TOOL_DISPLAY_SPECS[name as CursorNormalizedToolName];
423
+ if (Object.hasOwn(TOOL_DISPLAY_IMPLEMENTATIONS, name)) return TOOL_DISPLAY_IMPLEMENTATIONS[name as CursorNormalizedToolName];
464
424
  return undefined;
465
425
  }
466
426
 
@@ -1,8 +1,6 @@
1
1
  import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
- import { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
4
-
5
- export { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
3
+ import { asRecord, getArray, getRecord, getString, stringifyUnknown as stringifyUnknownValue } from "./cursor-record-utils.js";
6
4
 
7
5
  export interface TranscriptOptions {
8
6
  maxChars?: number;
@@ -47,30 +45,6 @@ export function isLocalReadPreviewContent(text: string): boolean {
47
45
  return text.startsWith(LOCAL_READ_PREVIEW_NOTICE);
48
46
  }
49
47
 
50
- export function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
51
- const value = record?.[key];
52
- return typeof value === "string" ? value : undefined;
53
- }
54
-
55
- export function getNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
56
- const value = record?.[key];
57
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
58
- }
59
-
60
- export function getBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
61
- const value = record?.[key];
62
- return typeof value === "boolean" ? value : undefined;
63
- }
64
-
65
- export function getRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
66
- return asRecord(record?.[key]);
67
- }
68
-
69
- export function getArray(record: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
70
- const value = record?.[key];
71
- return Array.isArray(value) ? value : undefined;
72
- }
73
-
74
48
  export function getToolName(toolCall: unknown): string {
75
49
  const record = asRecord(toolCall);
76
50
  return getString(record, "name") ?? getString(record, "type") ?? getString(record, "toolName") ?? "unknown";
@@ -96,13 +70,7 @@ export function normalizeResult(result: unknown): NormalizedResult {
96
70
  }
97
71
 
98
72
  export function stringifyUnknown(value: unknown): string {
99
- if (value === undefined) return "";
100
- if (typeof value === "string") return value;
101
- try {
102
- return JSON.stringify(value, null, 2) ?? String(value);
103
- } catch {
104
- return String(value);
105
- }
73
+ return stringifyUnknownValue(value, { pretty: true });
106
74
  }
107
75
 
108
76
  export function limitText(text: string, options: TranscriptOptions = {}, knownTotalLines?: number): string {
@@ -3,9 +3,7 @@ import {
3
3
  CURSOR_APPROX_CHARS_PER_TOKEN,
4
4
  CURSOR_IMAGE_TOKEN_ESTIMATE,
5
5
  estimateCursorContextTokens,
6
- estimateCursorPromptTokens,
7
6
  estimateCursorTextTokens,
8
- type CursorPrompt,
9
7
  type CursorPromptOptions,
10
8
  } from "./context.js";
11
9
 
@@ -28,10 +26,6 @@ export function getCursorPromptOptions(model: Model<Api>): CursorUsagePromptOpti
28
26
  };
29
27
  }
30
28
 
31
- export function estimateCursorPromptInputTokens(prompt: CursorPrompt, options: Pick<CursorPromptOptions, "charsPerToken" | "imageTokenEstimate">): number {
32
- return estimateCursorPromptTokens(prompt, options);
33
- }
34
-
35
29
  function stringifyUsageValue(value: unknown): string {
36
30
  try {
37
31
  return JSON.stringify(value) ?? "";
@@ -1,23 +1,15 @@
1
+ import { getFirstStringByKeys } from "./cursor-record-utils.js";
1
2
  import {
2
- classifyCursorWebToolKind as classifyCursorWebToolKindFromRegistry,
3
- type CursorWebToolKind,
3
+ classifyCursorWebToolKind,
4
+ normalizeCursorToolName as normalizeToolName,
4
5
  } from "./cursor-tool-presentation-registry.js";
5
- import { normalizeCursorToolName as normalizeToolName } from "./cursor-tool-presentation-registry.js";
6
- import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
7
-
8
- export type { CursorWebToolKind } from "./cursor-tool-presentation-registry.js";
9
- export { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
10
6
 
11
7
  function getMcpToolName(args: Record<string, unknown>): string | undefined {
12
- const toolName = typeof args.toolName === "string" ? args.toolName : typeof args.tool_name === "string" ? args.tool_name : undefined;
8
+ const toolName = getFirstStringByKeys(args, ["toolName", "tool_name"]);
13
9
  const trimmed = toolName?.trim();
14
10
  return trimmed || undefined;
15
11
  }
16
12
 
17
- export function classifyCursorWebToolKind(name: string | undefined): CursorWebToolKind | undefined {
18
- return classifyCursorWebToolKindFromRegistry(name);
19
- }
20
-
21
13
  /**
22
14
  * Maps SDK/host/MCP tool names to transcript display keys.
23
15
  * Web search/fetch often arrives as MCP `toolName` values, not dedicated SDK ToolTypes.