pi-cursor-sdk 0.1.18 → 0.1.20

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 (49) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +59 -1
  3. package/docs/cursor-live-smoke-checklist.md +4 -1
  4. package/docs/cursor-model-ux-spec.md +7 -5
  5. package/docs/cursor-native-tool-replay.md +99 -3
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +10 -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/probe-mcp-coldstart.mjs +244 -0
  13. package/scripts/validate-smoke-jsonl.mjs +27 -3
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-incomplete-tool-visibility.ts +124 -0
  18. package/src/cursor-live-run-coordinator.ts +18 -7
  19. package/src/cursor-mcp-timeout-override.ts +66 -11
  20. package/src/cursor-model.ts +12 -0
  21. package/src/cursor-native-tool-display-registration.ts +1 -4
  22. package/src/cursor-native-tool-display-replay.ts +65 -6
  23. package/src/cursor-native-tool-display-tools.ts +20 -0
  24. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  25. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  26. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  27. package/src/cursor-provider-errors.ts +96 -0
  28. package/src/cursor-provider-live-run-drain.ts +181 -62
  29. package/src/cursor-provider-turn-coordinator.ts +220 -33
  30. package/src/cursor-provider.ts +302 -93
  31. package/src/cursor-question-tool.ts +1 -4
  32. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  33. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  34. package/src/cursor-sdk-event-debug-session.ts +163 -0
  35. package/src/cursor-sdk-event-debug.ts +602 -0
  36. package/src/cursor-sensitive-text.ts +27 -7
  37. package/src/cursor-session-agent.ts +279 -82
  38. package/src/cursor-session-send-policy.ts +43 -0
  39. package/src/cursor-setting-sources.ts +29 -0
  40. package/src/cursor-state.ts +1 -5
  41. package/src/cursor-tool-lifecycle.ts +85 -0
  42. package/src/cursor-tool-names.ts +39 -0
  43. package/src/cursor-tool-transcript.ts +4 -2
  44. package/src/cursor-tool-visibility.ts +63 -0
  45. package/src/cursor-transcript-tool-formatters.ts +228 -5
  46. package/src/cursor-transcript-tool-specs.ts +135 -24
  47. package/src/cursor-transcript-utils.ts +12 -0
  48. package/src/cursor-web-tool-activity.ts +84 -0
  49. package/src/index.ts +4 -1
@@ -1,4 +1,10 @@
1
- import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayDisplayLabel, type CursorReplayLegacyToolName } from "./cursor-tool-names.js";
1
+ import {
2
+ CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME,
3
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
+ getCursorReplayActivityTitle,
5
+ getCursorReplayDisplayLabel,
6
+ type CursorReplayLegacyToolName,
7
+ } from "./cursor-tool-names.js";
2
8
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
3
9
  import {
4
10
  asRecord,
@@ -33,7 +39,11 @@ import {
33
39
  formatGrep,
34
40
  formatLs,
35
41
  formatMcp,
42
+ formatWebFetch,
43
+ formatWebSearch,
36
44
  formatPlan,
45
+ formatRecordScreen,
46
+ formatSemSearch,
37
47
  formatRead,
38
48
  formatReadLints,
39
49
  formatShell,
@@ -55,9 +65,14 @@ import {
55
65
  getTodoTotalCount,
56
66
  inferImageMimeType,
57
67
  summarizePlan,
68
+ summarizeMcp,
69
+ summarizeRecordScreen,
70
+ summarizeSemSearch,
58
71
  summarizeTask,
59
72
  summarizeTodos,
73
+ usesLocalReadPreview,
60
74
  } from "./cursor-transcript-tool-formatters.js";
75
+ import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
61
76
 
62
77
  export interface ToolDisplayContext {
63
78
  rawName: string;
@@ -105,19 +120,23 @@ function buildReplaySummaryDisplay(
105
120
  details: Record<string, unknown>,
106
121
  ): CursorPiToolDisplay {
107
122
  const isError = result.status === "error";
108
- const summary = isError ? formatError(result.error) : firstNonEmptyLine(contentText);
123
+ const summary = isError ? details.summary : (details.summary ?? firstNonEmptyLine(contentText));
109
124
  return {
110
125
  toolName,
111
126
  args,
112
127
  result: textToolResult(contentText, {
113
128
  ...details,
114
- summary: details.summary ?? summary,
129
+ summary,
115
130
  expandedText: details.expandedText ?? contentText,
116
131
  }),
117
132
  isError,
118
133
  };
119
134
  }
120
135
 
136
+ function getCursorToolActivityTitle(toolName: string): string {
137
+ return getCursorReplayActivityTitle(toolName) ?? buildGenericUnknownToolActivityTitle(toolName);
138
+ }
139
+
121
140
  function buildActivityReplayDisplay(cursorToolName: string, spec: ToolDisplaySpec, context: ToolDisplayContext): CursorPiToolDisplay {
122
141
  const activity = spec.activityReplay;
123
142
  if (!activity) throw new Error(`Missing activity replay spec for ${cursorToolName}`);
@@ -144,15 +163,34 @@ function buildActivityReplayDisplay(cursorToolName: string, spec: ToolDisplaySpe
144
163
  );
145
164
  }
146
165
 
166
+ function buildGenericUnknownToolActivityTitle(displayName: string): string {
167
+ if (displayName === "unknown") return "Cursor tool";
168
+ return `Cursor ${truncateArg(displayName)}`;
169
+ }
170
+
147
171
  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
- };
172
+ const { rawName, name, args, result, options } = context;
173
+ const displayName = rawName.trim() || name;
174
+ const activityTitle = buildGenericUnknownToolActivityTitle(displayName);
175
+ const contentText = formatFallback(name, args, result, options);
176
+ const fallbackBody = contentText.includes("\n\n") ? contentText.slice(contentText.indexOf("\n\n") + 2) : "";
177
+ const activitySummary =
178
+ result.status === "error" ? undefined : firstNonEmptyLine(fallbackBody);
179
+ const activityArgs = buildCursorActivityDisplayArgs(
180
+ { cursorToolName: displayName === "unknown" ? "tool" : displayName },
181
+ activityTitle,
182
+ activitySummary,
183
+ );
184
+ const summary =
185
+ result.status === "error"
186
+ ? undefined
187
+ : activitySummary ?? truncateArg(displayName === "unknown" ? "tool" : displayName);
188
+ return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, result, contentText, {
189
+ cursorToolName: name,
190
+ title: activityTitle,
191
+ summary,
192
+ expandedText: contentText,
193
+ });
156
194
  }
157
195
 
158
196
  function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
@@ -163,7 +201,7 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
163
201
  const nativeEditArgs = buildNativeEditDisplayArgs(rawName, args, options);
164
202
  const baseActivityArgs = buildCursorEditActivityDisplayArgs(args, options);
165
203
  const displayPath = typeof baseActivityArgs.path === "string" ? baseActivityArgs.path : undefined;
166
- const activityTitle = getCursorReplayDisplayLabel("cursor_edit");
204
+ const activityTitle = getCursorToolActivityTitle("edit");
167
205
  const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
168
206
  const contentText = formatEdit(activityArgs, result, options);
169
207
  const details = {
@@ -212,7 +250,7 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
212
250
  expandedText: contentText,
213
251
  };
214
252
  if (content === undefined) {
215
- const activityTitle = getCursorReplayDisplayLabel("cursor_write");
253
+ const activityTitle = getCursorToolActivityTitle("write");
216
254
  return buildReplaySummaryDisplay(
217
255
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
218
256
  buildCursorActivityDisplayArgs(displayArgs, activityTitle, displayPath ?? "file"),
@@ -238,10 +276,14 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
238
276
  formatTranscript: ({ args, result, options }) => formatRead(args, result, options),
239
277
  buildPiToolDisplay: ({ args, result, options }) => {
240
278
  const isError = result.status === "error";
279
+ const usesLocalPreview = !isError && usesLocalReadPreview(args, result, options);
241
280
  return {
242
281
  toolName: "read",
243
- args: buildReadDisplayArgs(args, options),
244
- result: textToolResult(isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options)),
282
+ args: buildReadDisplayArgs(args, options, result),
283
+ result: textToolResult(
284
+ isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options),
285
+ usesLocalPreview ? { localReadPreview: true } : undefined,
286
+ ),
245
287
  isError,
246
288
  };
247
289
  },
@@ -304,7 +346,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
304
346
  formatTranscript: ({ args, result, options }) => formatDelete(args, result, options),
305
347
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("delete", TOOL_DISPLAY_SPECS.delete, context),
306
348
  activityReplay: {
307
- labelKey: "cursor_delete",
349
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.delete,
308
350
  buildActivityArgs: ({ args, options }) => {
309
351
  const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
310
352
  return displayPath ? { path: displayPath } : {};
@@ -328,7 +370,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
328
370
  formatTranscript: ({ args, result, options }) => formatReadLints(args, result, options),
329
371
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("readLints", TOOL_DISPLAY_SPECS.readLints, context),
330
372
  activityReplay: {
331
- labelKey: "cursor_read_lints",
373
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.readLints,
332
374
  buildActivityArgs: ({ args, result, options }) => {
333
375
  const paths = getReadLintPaths(args, result, options);
334
376
  const diagnosticCount = getReadLintDiagnostics(result, options).length;
@@ -346,7 +388,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
346
388
  formatTranscript: ({ args, result, options }) => formatTodos(args, result, options, "updateTodos"),
347
389
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("updateTodos", TOOL_DISPLAY_SPECS.updateTodos, context),
348
390
  activityReplay: {
349
- labelKey: "cursor_update_todos",
391
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.updateTodos,
350
392
  buildActivityArgs: ({ args, result }) => {
351
393
  const todos = getTodoItems(args, result);
352
394
  return { totalCount: getTodoTotalCount(args, result, todos) };
@@ -359,7 +401,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
359
401
  formatTranscript: ({ args, result, options }) => formatPlan(args, result, options),
360
402
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("createPlan", TOOL_DISPLAY_SPECS.createPlan, context),
361
403
  activityReplay: {
362
- labelKey: "cursor_create_plan",
404
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.createPlan,
363
405
  buildActivityArgs: ({ args, result }) => {
364
406
  const todos = getTodoItems(args, result);
365
407
  return { totalCount: getTodoTotalCount(args, result, todos) };
@@ -372,7 +414,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
372
414
  formatTranscript: ({ args, result, options }) => formatTask(args, result, options),
373
415
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("task", TOOL_DISPLAY_SPECS.task, context),
374
416
  activityReplay: {
375
- labelKey: "cursor_task",
417
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.task,
376
418
  buildActivityArgs: ({ args, result }) => {
377
419
  const description = getTaskDescription(args, result);
378
420
  return { description: truncateArg(description) };
@@ -388,7 +430,7 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
388
430
  formatTranscript: ({ args, result, options }) => formatGenerateImage(args, result, options),
389
431
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("generateImage", TOOL_DISPLAY_SPECS.generateImage, context),
390
432
  activityReplay: {
391
- labelKey: "cursor_generate_image",
433
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.generateImage,
392
434
  buildActivityArgs: ({ args }) => {
393
435
  const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
394
436
  return { prompt: truncateArg(prompt) };
@@ -415,14 +457,83 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
415
457
  formatTranscript: ({ args, result, options }) => formatMcp(args, result, options),
416
458
  buildPiToolDisplay: (context) => buildActivityReplayDisplay("mcp", TOOL_DISPLAY_SPECS.mcp, context),
417
459
  activityReplay: {
418
- labelKey: "cursor_mcp",
460
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.mcp,
419
461
  buildActivityArgs: ({ args }) => {
420
462
  const toolName = getString(args, "toolName") ?? "mcp";
421
463
  return { toolName: truncateArg(toolName) };
422
464
  },
423
- buildActivitySummary: ({ args }) => truncateArg(getString(args, "toolName") ?? "mcp"),
465
+ buildActivitySummary: ({ args, result }) => summarizeMcp(args, result),
466
+ buildDetails: ({ args, result }) => ({
467
+ summary: result.status === "error" ? undefined : summarizeMcp(args, result),
468
+ }),
469
+ },
470
+ },
471
+ semSearch: {
472
+ formatTranscript: ({ args, result, options }) => formatSemSearch(args, result, options),
473
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", TOOL_DISPLAY_SPECS.semSearch, context),
474
+ activityReplay: {
475
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.semSearch,
476
+ buildActivityArgs: ({ args }) => {
477
+ const query = getString(args, "query") ?? "semantic search";
478
+ return { query: truncateArg(query) };
479
+ },
480
+ buildActivitySummary: ({ args }) => summarizeSemSearch(args),
481
+ buildDetails: ({ result }, contentText) => ({
482
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "semantic search results captured",
483
+ }),
484
+ },
485
+ },
486
+ recordScreen: {
487
+ formatTranscript: ({ args, result, options }) => formatRecordScreen(args, result, options),
488
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", TOOL_DISPLAY_SPECS.recordScreen, context),
489
+ activityReplay: {
490
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.recordScreen,
491
+ buildActivityArgs: ({ args, result, options }) => {
492
+ const mode = getString(args, "mode");
493
+ const path = getString(asRecord(result.value), "path");
494
+ return {
495
+ ...(mode ? { mode } : {}),
496
+ ...(path ? { path: formatDisplayPath(path, options.cwd) } : {}),
497
+ };
498
+ },
499
+ buildActivitySummary: ({ args, result, options }) => summarizeRecordScreen(args, result, options),
500
+ buildDetails: ({ args, result, options }, contentText) => ({
501
+ summary:
502
+ result.status === "error"
503
+ ? undefined
504
+ : summarizeRecordScreen(args, result, options) ?? firstNonEmptyLine(contentText) ?? "screen recording updated",
505
+ }),
506
+ },
507
+ },
508
+ webSearch: {
509
+ formatTranscript: ({ args, result, options }) => formatWebSearch(args, result, options),
510
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", TOOL_DISPLAY_SPECS.webSearch, context),
511
+ activityReplay: {
512
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.webSearch,
513
+ buildActivityArgs: ({ args }) => {
514
+ const query = extractWebSearchQuery(args);
515
+ return query ? { query: truncateArg(query) } : {};
516
+ },
517
+ buildActivitySummary: ({ args }) => truncateArg(extractWebSearchQuery(args) ?? "web search"),
518
+ buildDetails: ({ result }, contentText) => ({
519
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web search result captured",
520
+ collapseDetailsByDefault: true,
521
+ }),
522
+ },
523
+ },
524
+ webFetch: {
525
+ formatTranscript: ({ args, result, options }) => formatWebFetch(args, result, options),
526
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", TOOL_DISPLAY_SPECS.webFetch, context),
527
+ activityReplay: {
528
+ labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.webFetch,
529
+ buildActivityArgs: ({ args }) => {
530
+ const target = extractWebFetchTarget(args);
531
+ return target ? { url: truncateArg(target) } : {};
532
+ },
533
+ buildActivitySummary: ({ args }) => truncateArg(extractWebFetchTarget(args) ?? "web fetch"),
424
534
  buildDetails: ({ result }, contentText) => ({
425
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "MCP result captured",
535
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web fetch result captured",
536
+ collapseDetailsByDefault: true,
426
537
  }),
427
538
  },
428
539
  },
@@ -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) => {