pi-cursor-sdk 0.1.18 → 0.1.19

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 (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/docs/cursor-live-smoke-checklist.md +3 -0
  4. package/docs/cursor-model-ux-spec.md +4 -3
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +8 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  11. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  12. package/scripts/validate-smoke-jsonl.mjs +27 -3
  13. package/src/context.ts +45 -32
  14. package/src/cursor-agent-message-web-tools.ts +172 -0
  15. package/src/cursor-agents-context.ts +176 -0
  16. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  17. package/src/cursor-live-run-coordinator.ts +18 -7
  18. package/src/cursor-model.ts +12 -0
  19. package/src/cursor-native-tool-display-registration.ts +1 -4
  20. package/src/cursor-native-tool-display-replay.ts +63 -5
  21. package/src/cursor-native-tool-display-tools.ts +20 -0
  22. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  23. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  24. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  25. package/src/cursor-provider-errors.ts +96 -0
  26. package/src/cursor-provider-live-run-drain.ts +181 -62
  27. package/src/cursor-provider-turn-coordinator.ts +198 -32
  28. package/src/cursor-provider.ts +270 -83
  29. package/src/cursor-question-tool.ts +1 -4
  30. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  31. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  32. package/src/cursor-sdk-event-debug-session.ts +163 -0
  33. package/src/cursor-sdk-event-debug.ts +597 -0
  34. package/src/cursor-sensitive-text.ts +27 -7
  35. package/src/cursor-session-agent.ts +25 -3
  36. package/src/cursor-session-send-policy.ts +43 -0
  37. package/src/cursor-setting-sources.ts +29 -0
  38. package/src/cursor-state.ts +1 -5
  39. package/src/cursor-tool-lifecycle.ts +111 -0
  40. package/src/cursor-tool-names.ts +12 -0
  41. package/src/cursor-tool-transcript.ts +4 -2
  42. package/src/cursor-transcript-tool-formatters.ts +228 -5
  43. package/src/cursor-transcript-tool-specs.ts +113 -14
  44. package/src/cursor-transcript-utils.ts +12 -0
  45. package/src/cursor-web-tool-activity.ts +84 -0
  46. package/src/index.ts +4 -1
@@ -33,7 +33,11 @@ import {
33
33
  formatGrep,
34
34
  formatLs,
35
35
  formatMcp,
36
+ formatWebFetch,
37
+ formatWebSearch,
36
38
  formatPlan,
39
+ formatRecordScreen,
40
+ formatSemSearch,
37
41
  formatRead,
38
42
  formatReadLints,
39
43
  formatShell,
@@ -55,9 +59,14 @@ import {
55
59
  getTodoTotalCount,
56
60
  inferImageMimeType,
57
61
  summarizePlan,
62
+ summarizeMcp,
63
+ summarizeRecordScreen,
64
+ summarizeSemSearch,
58
65
  summarizeTask,
59
66
  summarizeTodos,
67
+ usesLocalReadPreview,
60
68
  } from "./cursor-transcript-tool-formatters.js";
69
+ import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
61
70
 
62
71
  export interface ToolDisplayContext {
63
72
  rawName: string;
@@ -105,13 +114,13 @@ function buildReplaySummaryDisplay(
105
114
  details: Record<string, unknown>,
106
115
  ): CursorPiToolDisplay {
107
116
  const isError = result.status === "error";
108
- const summary = isError ? formatError(result.error) : firstNonEmptyLine(contentText);
117
+ const summary = isError ? details.summary : (details.summary ?? firstNonEmptyLine(contentText));
109
118
  return {
110
119
  toolName,
111
120
  args,
112
121
  result: textToolResult(contentText, {
113
122
  ...details,
114
- summary: details.summary ?? summary,
123
+ summary,
115
124
  expandedText: details.expandedText ?? contentText,
116
125
  }),
117
126
  isError,
@@ -144,15 +153,34 @@ function buildActivityReplayDisplay(cursorToolName: string, spec: ToolDisplaySpe
144
153
  );
145
154
  }
146
155
 
156
+ function buildGenericUnknownToolActivityTitle(displayName: string): string {
157
+ if (displayName === "unknown") return "Cursor tool";
158
+ return `Cursor ${truncateArg(displayName)}`;
159
+ }
160
+
147
161
  function buildGenericPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
148
- const { name, args, result, options } = context;
149
- const isError = result.status === "error";
150
- return {
151
- toolName: name,
152
- args,
153
- result: textToolResult(isError ? formatError(result.error) : limitText(stringifyUnknown(result.value), options)),
154
- isError,
155
- };
162
+ const { rawName, name, args, result, options } = context;
163
+ const displayName = rawName.trim() || name;
164
+ const activityTitle = buildGenericUnknownToolActivityTitle(displayName);
165
+ const contentText = formatFallback(name, args, result, options);
166
+ const fallbackBody = contentText.includes("\n\n") ? contentText.slice(contentText.indexOf("\n\n") + 2) : "";
167
+ const activitySummary =
168
+ result.status === "error" ? undefined : firstNonEmptyLine(fallbackBody);
169
+ const activityArgs = buildCursorActivityDisplayArgs(
170
+ { cursorToolName: displayName === "unknown" ? "tool" : displayName },
171
+ activityTitle,
172
+ activitySummary,
173
+ );
174
+ const summary =
175
+ result.status === "error"
176
+ ? undefined
177
+ : activitySummary ?? truncateArg(displayName === "unknown" ? "tool" : displayName);
178
+ return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, result, contentText, {
179
+ cursorToolName: name,
180
+ title: activityTitle,
181
+ summary,
182
+ expandedText: contentText,
183
+ });
156
184
  }
157
185
 
158
186
  function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
@@ -238,10 +266,14 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
238
266
  formatTranscript: ({ args, result, options }) => formatRead(args, result, options),
239
267
  buildPiToolDisplay: ({ args, result, options }) => {
240
268
  const isError = result.status === "error";
269
+ const usesLocalPreview = !isError && usesLocalReadPreview(args, result, options);
241
270
  return {
242
271
  toolName: "read",
243
- args: buildReadDisplayArgs(args, options),
244
- result: textToolResult(isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options)),
272
+ args: buildReadDisplayArgs(args, options, result),
273
+ result: textToolResult(
274
+ isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options),
275
+ usesLocalPreview ? { localReadPreview: true } : undefined,
276
+ ),
245
277
  isError,
246
278
  };
247
279
  },
@@ -420,9 +452,76 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
420
452
  const toolName = getString(args, "toolName") ?? "mcp";
421
453
  return { toolName: truncateArg(toolName) };
422
454
  },
423
- buildActivitySummary: ({ args }) => truncateArg(getString(args, "toolName") ?? "mcp"),
455
+ buildActivitySummary: ({ args, result }) => summarizeMcp(args, result),
456
+ buildDetails: ({ args, result }) => ({
457
+ summary: result.status === "error" ? undefined : summarizeMcp(args, result),
458
+ }),
459
+ },
460
+ },
461
+ semSearch: {
462
+ formatTranscript: ({ args, result, options }) => formatSemSearch(args, result, options),
463
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", TOOL_DISPLAY_SPECS.semSearch, context),
464
+ activityReplay: {
465
+ labelKey: "cursor_sem_search",
466
+ buildActivityArgs: ({ args }) => {
467
+ const query = getString(args, "query") ?? "semantic search";
468
+ return { query: truncateArg(query) };
469
+ },
470
+ buildActivitySummary: ({ args }) => summarizeSemSearch(args),
471
+ buildDetails: ({ result }, contentText) => ({
472
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "semantic search results captured",
473
+ }),
474
+ },
475
+ },
476
+ recordScreen: {
477
+ formatTranscript: ({ args, result, options }) => formatRecordScreen(args, result, options),
478
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", TOOL_DISPLAY_SPECS.recordScreen, context),
479
+ activityReplay: {
480
+ labelKey: "cursor_record_screen",
481
+ buildActivityArgs: ({ args, result, options }) => {
482
+ const mode = getString(args, "mode");
483
+ const path = getString(asRecord(result.value), "path");
484
+ return {
485
+ ...(mode ? { mode } : {}),
486
+ ...(path ? { path: formatDisplayPath(path, options.cwd) } : {}),
487
+ };
488
+ },
489
+ buildActivitySummary: ({ args, result, options }) => summarizeRecordScreen(args, result, options),
490
+ buildDetails: ({ args, result, options }, contentText) => ({
491
+ summary:
492
+ result.status === "error"
493
+ ? undefined
494
+ : summarizeRecordScreen(args, result, options) ?? firstNonEmptyLine(contentText) ?? "screen recording updated",
495
+ }),
496
+ },
497
+ },
498
+ webSearch: {
499
+ formatTranscript: ({ args, result, options }) => formatWebSearch(args, result, options),
500
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", TOOL_DISPLAY_SPECS.webSearch, context),
501
+ activityReplay: {
502
+ labelKey: "cursor_web_search",
503
+ buildActivityArgs: ({ args }) => {
504
+ const query = extractWebSearchQuery(args);
505
+ return query ? { query: truncateArg(query) } : {};
506
+ },
507
+ buildActivitySummary: ({ args }) => truncateArg(extractWebSearchQuery(args) ?? "web search"),
508
+ buildDetails: ({ result }, contentText) => ({
509
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web search result captured",
510
+ }),
511
+ },
512
+ },
513
+ webFetch: {
514
+ formatTranscript: ({ args, result, options }) => formatWebFetch(args, result, options),
515
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", TOOL_DISPLAY_SPECS.webFetch, context),
516
+ activityReplay: {
517
+ labelKey: "cursor_web_fetch",
518
+ buildActivityArgs: ({ args }) => {
519
+ const target = extractWebFetchTarget(args);
520
+ return target ? { url: truncateArg(target) } : {};
521
+ },
522
+ buildActivitySummary: ({ args }) => truncateArg(extractWebFetchTarget(args) ?? "web fetch"),
424
523
  buildDetails: ({ result }, contentText) => ({
425
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "MCP result captured",
524
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web fetch result captured",
426
525
  }),
427
526
  },
428
527
  },
@@ -43,6 +43,10 @@ export const DEFAULT_NATIVE_READ_DISPLAY_LINES = 20;
43
43
  export const LOCAL_READ_PREVIEW_NOTICE =
44
44
  "[local file preview at transcript time; Cursor read result content was unavailable]";
45
45
 
46
+ export function isLocalReadPreviewContent(text: string): boolean {
47
+ return text.startsWith(LOCAL_READ_PREVIEW_NOTICE);
48
+ }
49
+
46
50
  export function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
47
51
  const value = record?.[key];
48
52
  return typeof value === "string" ? value : undefined;
@@ -113,6 +117,14 @@ export function normalizeToolName(name: string): string {
113
117
  case "notebook_edit":
114
118
  case "notebookedit":
115
119
  return "edit";
120
+ case "websearch":
121
+ case "web_search":
122
+ case "web-search":
123
+ return "webSearch";
124
+ case "webfetch":
125
+ case "web_fetch":
126
+ case "web-fetch":
127
+ return "webFetch";
116
128
  default:
117
129
  return normalized || "unknown";
118
130
  }
@@ -0,0 +1,84 @@
1
+ import { normalizeToolName } from "./cursor-transcript-utils.js";
2
+
3
+ export type CursorWebToolKind = "webSearch" | "webFetch";
4
+
5
+ const WEB_SEARCH_NAME_PATTERN =
6
+ /^(?:web[-_ ]?search|search[-_ ]?web|websearch|browser[-_ ]?search|cursor[-_ ]?web[-_ ]?search)$/i;
7
+ const WEB_FETCH_NAME_PATTERN =
8
+ /^(?:web[-_ ]?fetch|fetch[-_ ]?web|webfetch|browser[-_ ]?fetch|fetch[-_ ]?url|cursor[-_ ]?web[-_ ]?fetch)$/i;
9
+
10
+ function normalizeWebToolLookupName(name: string): string {
11
+ return name.replace(/\s+/g, " ").trim().toLowerCase();
12
+ }
13
+
14
+ export function classifyCursorWebToolKind(name: string | undefined): CursorWebToolKind | undefined {
15
+ if (!name) return undefined;
16
+ const normalized = normalizeWebToolLookupName(name);
17
+ if (WEB_SEARCH_NAME_PATTERN.test(normalized) || normalized === "websearch" || normalized === "web_search") {
18
+ return "webSearch";
19
+ }
20
+ if (WEB_FETCH_NAME_PATTERN.test(normalized) || normalized === "webfetch" || normalized === "web_fetch") {
21
+ return "webFetch";
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ function getNestedMcpArgs(args: Record<string, unknown>): Record<string, unknown> {
27
+ const nested = args.args;
28
+ return nested && typeof nested === "object" && !Array.isArray(nested) ? (nested as Record<string, unknown>) : {};
29
+ }
30
+
31
+ function getMcpToolName(args: Record<string, unknown>): string | undefined {
32
+ const toolName = typeof args.toolName === "string" ? args.toolName : typeof args.tool_name === "string" ? args.tool_name : undefined;
33
+ const trimmed = toolName?.trim();
34
+ return trimmed || undefined;
35
+ }
36
+
37
+ function firstNonEmptyString(...values: Array<string | undefined>): string | undefined {
38
+ for (const value of values) {
39
+ const trimmed = value?.trim();
40
+ if (trimmed) return trimmed;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ export function extractWebSearchQuery(args: Record<string, unknown>): string | undefined {
46
+ const nested = getNestedMcpArgs(args);
47
+ return firstNonEmptyString(
48
+ typeof args.search_term === "string" ? args.search_term : undefined,
49
+ typeof args.searchTerm === "string" ? args.searchTerm : undefined,
50
+ typeof args.query === "string" ? args.query : undefined,
51
+ typeof args.q === "string" ? args.q : undefined,
52
+ typeof nested.search_term === "string" ? nested.search_term : undefined,
53
+ typeof nested.searchTerm === "string" ? nested.searchTerm : undefined,
54
+ typeof nested.query === "string" ? nested.query : undefined,
55
+ typeof nested.q === "string" ? nested.q : undefined,
56
+ );
57
+ }
58
+
59
+ export function extractWebFetchTarget(args: Record<string, unknown>): string | undefined {
60
+ const nested = getNestedMcpArgs(args);
61
+ return firstNonEmptyString(
62
+ typeof args.url === "string" ? args.url : undefined,
63
+ typeof args.uri === "string" ? args.uri : undefined,
64
+ typeof args.href === "string" ? args.href : undefined,
65
+ typeof nested.url === "string" ? nested.url : undefined,
66
+ typeof nested.uri === "string" ? nested.uri : undefined,
67
+ typeof nested.href === "string" ? nested.href : undefined,
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Maps SDK/host/MCP tool names to transcript display keys.
73
+ * Web search/fetch often arrives as MCP `toolName` values, not dedicated SDK ToolTypes.
74
+ */
75
+ export function resolveTranscriptToolName(rawName: string, args: Record<string, unknown>): string {
76
+ const normalized = normalizeToolName(rawName);
77
+ const directWebKind = classifyCursorWebToolKind(rawName) ?? classifyCursorWebToolKind(normalized);
78
+ if (directWebKind) return directWebKind;
79
+ if (normalized === "mcp") {
80
+ const mcpWebKind = classifyCursorWebToolKind(getMcpToolName(args));
81
+ if (mcpWebKind) return mcpWebKind;
82
+ }
83
+ return normalized;
84
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display.js
5
5
  import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
6
6
  import { registerCursorQuestionTool } from "./cursor-question-tool.js";
7
7
  import { registerCursorSessionCwd } from "./cursor-session-cwd.js";
8
+ import { registerCursorAgentsContextDedup } from "./cursor-agents-context.js";
8
9
  import { registerCursorSessionAgent } from "./cursor-session-agent.js";
9
10
  import { streamCursor } from "./cursor-provider.js";
10
11
 
@@ -21,7 +22,8 @@ type CursorExtensionApi =
21
22
  & Parameters<typeof registerCursorFastControls>[0]
22
23
  & Parameters<typeof registerCursorNativeToolDisplay>[0]
23
24
  & Parameters<typeof registerCursorQuestionTool>[0]
24
- & Parameters<typeof registerCursorPiToolBridge>[0];
25
+ & Parameters<typeof registerCursorPiToolBridge>[0]
26
+ & Parameters<typeof registerCursorAgentsContextDedup>[0];
25
27
 
26
28
  function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
27
29
  return {
@@ -46,6 +48,7 @@ export default async function (pi: CursorExtensionApi) {
46
48
  registerCursorNativeToolDisplay(pi);
47
49
  registerCursorQuestionTool(pi);
48
50
  registerCursorPiToolBridge(pi);
51
+ registerCursorAgentsContextDedup(pi);
49
52
  let fallbackIssue: CursorModelFallbackIssue | undefined;
50
53
  const models = await discoverModels({
51
54
  onFallback: (issue) => {