pi-cursor-sdk 0.1.37 → 0.1.39

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 (77) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/docs/cursor-model-ux-spec.md +1 -1
  4. package/docs/cursor-native-tool-replay.md +5 -5
  5. package/package.json +1 -1
  6. package/scripts/platform-smoke/card-detect.mjs +1 -1
  7. package/src/context-window-cache.ts +10 -14
  8. package/src/context.ts +1 -1
  9. package/src/cursor-agent-message-web-tools.ts +2 -1
  10. package/src/cursor-agents-context-registration.ts +18 -0
  11. package/src/cursor-agents-context.ts +21 -30
  12. package/src/cursor-edit-diff.ts +4 -2
  13. package/src/cursor-fallback-warning.ts +22 -0
  14. package/src/cursor-incomplete-tool-visibility.ts +5 -10
  15. package/src/cursor-live-run-coordinator.ts +1 -1
  16. package/src/cursor-mcp-timeout-override.ts +0 -2
  17. package/src/cursor-model-lifecycle.ts +72 -0
  18. package/src/cursor-native-replay-routing.ts +1 -1
  19. package/src/cursor-native-replay-trace.ts +1 -1
  20. package/src/cursor-native-tool-display-registration.ts +16 -28
  21. package/src/cursor-native-tool-display-replay.ts +4 -21
  22. package/src/cursor-native-tool-display-state.ts +1 -1
  23. package/src/cursor-native-tool-display-tools.ts +10 -17
  24. package/src/cursor-native-tool-names.ts +16 -0
  25. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  26. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  27. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  28. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  29. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  30. package/src/cursor-pi-tool-bridge.ts +7 -7
  31. package/src/cursor-provider-errors.ts +11 -4
  32. package/src/cursor-provider-lazy.ts +51 -0
  33. package/src/cursor-provider-live-run-drain.ts +1 -1
  34. package/src/cursor-provider-run-finalizer.ts +5 -5
  35. package/src/cursor-provider-run-outcome.ts +0 -1
  36. package/src/cursor-provider-turn-coordinator.ts +16 -6
  37. package/src/cursor-provider-turn-display-router.ts +5 -1
  38. package/src/cursor-provider-turn-emit.ts +1 -1
  39. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  40. package/src/cursor-provider-turn-prepare.ts +13 -9
  41. package/src/cursor-provider-turn-runner.ts +3 -11
  42. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  43. package/src/cursor-provider-turn-send.ts +7 -2
  44. package/src/cursor-provider-turn-shell-output.ts +38 -3
  45. package/src/cursor-provider-turn-types.ts +1 -3
  46. package/src/cursor-provider.ts +3 -2
  47. package/src/cursor-question-tool.ts +5 -18
  48. package/src/cursor-record-utils.ts +42 -0
  49. package/src/cursor-replay-activity-builders.ts +16 -122
  50. package/src/cursor-replay-tool-details.ts +52 -80
  51. package/src/cursor-sdk-event-debug.ts +6 -6
  52. package/src/cursor-sensitive-text.ts +4 -4
  53. package/src/cursor-session-agent-lifecycle.ts +47 -0
  54. package/src/cursor-session-agent.ts +9 -47
  55. package/src/cursor-session-scope.ts +23 -4
  56. package/src/cursor-setting-sources.ts +8 -8
  57. package/src/cursor-skill-tool.ts +25 -32
  58. package/src/cursor-state.ts +66 -45
  59. package/src/cursor-tool-lifecycle.ts +22 -10
  60. package/src/cursor-tool-presentation-registry.ts +27 -18
  61. package/src/cursor-tool-result-display-readers.ts +185 -0
  62. package/src/cursor-tool-transcript.ts +17 -33
  63. package/src/cursor-tool-visibility.ts +9 -1
  64. package/src/cursor-transcript-tool-formatters.ts +23 -172
  65. package/src/cursor-transcript-tool-specs.ts +16 -41
  66. package/src/cursor-transcript-utils.ts +2 -34
  67. package/src/cursor-usage-accounting.ts +0 -6
  68. package/src/cursor-web-tool-activity.ts +4 -12
  69. package/src/cursor-web-tool-args.ts +1 -9
  70. package/src/index.ts +15 -16
  71. package/src/model-discovery.ts +5 -4
  72. package/src/model-list-cache.ts +37 -38
  73. package/src/cursor-native-tool-display.ts +0 -10
  74. package/src/cursor-provider-turn-api-key.ts +0 -1
  75. package/src/cursor-provider-turn-message-offset.ts +0 -15
  76. package/src/cursor-session-cwd.ts +0 -28
  77. package/src/cursor-tool-names.ts +0 -9
@@ -1,12 +1,12 @@
1
- import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent, TurnStartEvent } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
4
4
  isNativeCursorToolName,
5
5
  NATIVE_CURSOR_TOOL_NAMES,
6
- registerNativeCursorTool,
7
6
  type NativeCursorToolName,
8
- } from "./cursor-native-tool-display-tools.js";
7
+ } from "./cursor-native-tool-names.js";
9
8
  import { isCursorModel } from "./cursor-model.js";
9
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
10
10
  import {
11
11
  isCursorNativeToolDisplayRequested,
12
12
  isCursorNativeToolRegistrationRequested,
@@ -15,7 +15,7 @@ import {
15
15
  registeredNativeToolNames,
16
16
  skippedNativeToolNames,
17
17
  } from "./cursor-native-tool-display-state.js";
18
- import { isCursorReplayToolName } from "./cursor-tool-names.js";
18
+ import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
19
19
 
20
20
  export const CURSOR_CORE_PI_REPLAY_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
21
21
  const CORE_PI_TOOL_NAMES = new Set<string>(CURSOR_CORE_PI_REPLAY_TOOL_NAMES);
@@ -27,12 +27,7 @@ function isCursorCorePiReplayToolName(toolName: string): toolName is (typeof CUR
27
27
  type CursorNativeToolActivationApi = Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">;
28
28
  type CursorNativeToolRegistryApi = CursorNativeToolActivationApi & Pick<ExtensionAPI, "getAllTools" | "registerTool">;
29
29
 
30
- export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
31
- on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
32
- on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
33
- on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
34
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
35
- }
30
+ export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi, CursorModelLifecycleExtensionApi {}
36
31
 
37
32
  function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: NativeCursorToolName): boolean {
38
33
  const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
@@ -43,11 +38,12 @@ type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
43
38
  ui: Pick<ExtensionContext["ui"], "notify">;
44
39
  };
45
40
 
46
- function registerNativeCursorToolsFromSet(
41
+ async function registerNativeCursorToolsFromSet(
47
42
  pi: CursorNativeToolRegistryApi,
48
43
  toolNames: readonly NativeCursorToolName[],
49
- ): NativeCursorToolName[] {
44
+ ): Promise<NativeCursorToolName[]> {
50
45
  const newlySkippedToolNames: NativeCursorToolName[] = [];
46
+ let registerNativeCursorTool: ((pi: CursorNativeToolRegistryApi, toolName: NativeCursorToolName) => void) | undefined;
51
47
  for (const toolName of toolNames) {
52
48
  if (registeredNativeToolNames.has(toolName) || skippedNativeToolNames.has(toolName)) continue;
53
49
  if (hasNonBuiltinTool(pi, toolName)) {
@@ -55,6 +51,7 @@ function registerNativeCursorToolsFromSet(
55
51
  newlySkippedToolNames.push(toolName);
56
52
  continue;
57
53
  }
54
+ registerNativeCursorTool ??= (await import("./cursor-native-tool-display-tools.js")).registerNativeCursorTool;
58
55
  registerNativeCursorTool(pi, toolName);
59
56
  registeredNativeToolNames.add(toolName);
60
57
  }
@@ -97,7 +94,7 @@ export function syncRegisteredNativeCursorToolsForModel(
97
94
  if (changed) pi.setActiveTools([...activeToolNames]);
98
95
  }
99
96
 
100
- function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
97
+ async function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
101
98
  if (!isCursorNativeToolRegistrationRequested()) {
102
99
  registeredNativeToolNames.clear();
103
100
  skippedNativeToolNames.clear();
@@ -107,31 +104,22 @@ function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryA
107
104
 
108
105
  const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
109
106
  const skippedToolNames = [
110
- ...registerNativeCursorToolsFromSet(pi, nonCoreToolNames),
111
- ...registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES),
107
+ ...(await registerNativeCursorToolsFromSet(pi, nonCoreToolNames)),
108
+ ...(await registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES)),
112
109
  ];
113
110
  notifySkippedNativeCursorToolsIfNeeded(ctx, skippedToolNames);
114
111
  }
115
112
 
116
- function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
113
+ async function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
117
114
  if (isCursorModel(ctx.model) && !hasAttemptedNativeCursorToolRegistration()) {
118
- ensureNativeCursorToolsRegisteredForModel(pi, ctx);
115
+ await ensureNativeCursorToolsRegisteredForModel(pi, ctx);
119
116
  }
120
117
  syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
121
118
  }
122
119
 
123
120
  export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
124
- pi.on("session_start", (_event, ctx) => {
125
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
126
- });
127
- pi.on("before_agent_start", (_event, ctx) => {
128
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
129
- });
130
- pi.on("turn_start", (_event, ctx) => {
131
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
132
- });
133
- pi.on("model_select", (event, ctx) => {
134
- ensureThenSyncNativeCursorToolsForModel(pi, { ...ctx, model: event.model });
121
+ registerCursorModelLifecycle(pi, async (ctx) => {
122
+ await ensureThenSyncNativeCursorToolsForModel(pi, ctx);
135
123
  });
136
124
  }
137
125
 
@@ -1,9 +1,10 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
- import { basename, extname } from "node:path";
2
+ import { basename } from "node:path";
3
3
  import { getLanguageFromPath, highlightCode, type ToolDefinition } from "@earendil-works/pi-coding-agent";
4
4
  import { Image, Text, type Component } from "@earendil-works/pi-tui";
5
5
  import { Type } from "typebox";
6
6
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
7
+ import { inferImageMimeType } from "./cursor-tool-result-display-readers.js";
7
8
  import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
8
9
  import {
9
10
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
@@ -17,7 +18,6 @@ import {
17
18
  type CursorReplayActivityDetails,
18
19
  type CursorReplayToolDetails,
19
20
  type CursorReplayNativeWriteDetails,
20
- isCursorReplayNativeEditDetails,
21
21
  isCursorReplayGenerateImageDetails,
22
22
  isCursorReplayActivityDetails,
23
23
  parseCursorReplayToolDetails,
@@ -49,22 +49,6 @@ type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayTool
49
49
  type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
50
50
  export type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
51
51
 
52
- function inferImageMimeTypeFromPath(path: string | undefined): string | undefined {
53
- switch (extname(path ?? "").toLowerCase()) {
54
- case ".png":
55
- return "image/png";
56
- case ".jpg":
57
- case ".jpeg":
58
- return "image/jpeg";
59
- case ".gif":
60
- return "image/gif";
61
- case ".webp":
62
- return "image/webp";
63
- default:
64
- return undefined;
65
- }
66
- }
67
-
68
52
  function readImageFileForReplay(path: string | undefined): string | undefined {
69
53
  if (!path) return undefined;
70
54
  try {
@@ -464,7 +448,7 @@ function renderExpandableCursorReplayResult(
464
448
  }
465
449
  if (details.imagePath && !isError && context.showImages) {
466
450
  const imageData = readImageFileForReplay(details.imagePath);
467
- const mimeType = details.imageMimeType ?? inferImageMimeTypeFromPath(details.imagePath);
451
+ const mimeType = details.imageMimeType ?? inferImageMimeType(details.imagePath);
468
452
  if (imageData && mimeType) return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
469
453
  }
470
454
  return new Text(rendered, 0, 0);
@@ -485,7 +469,6 @@ function renderCursorReplayEditResult(
485
469
  function renderCursorReplayWriteResult(
486
470
  details: CursorReplayNativeWriteDetails,
487
471
  result: Parameters<CursorReplayRenderResult>[0],
488
- options: Parameters<CursorReplayRenderResult>[1],
489
472
  theme: Parameters<CursorReplayRenderResult>[2],
490
473
  ): Component {
491
474
  const text = firstContentText(result);
@@ -532,7 +515,7 @@ function renderCursorReplayDetails(
532
515
  case "nativeEdit":
533
516
  return renderCursorReplayEditResult(details, options, theme);
534
517
  case "nativeWrite":
535
- return renderCursorReplayWriteResult(details, result, options, theme);
518
+ return renderCursorReplayWriteResult(details, result, theme);
536
519
  case "generateImage":
537
520
  return renderCursorGenerateImageResult(details, result, options, theme, context, isError);
538
521
  case "activity":
@@ -1,4 +1,4 @@
1
- import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
1
+ import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
2
  import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
3
3
 
4
4
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
@@ -10,11 +10,17 @@ import {
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
  import { Text } from "@earendil-works/pi-tui";
12
12
  import type { TSchema } from "typebox";
13
- import { getCursorSessionCwd } from "./cursor-session-cwd.js";
13
+ import { getCursorSessionCwd } from "./cursor-session-scope.js";
14
14
  import {
15
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
16
- isCursorReplayToolName,
17
- } from "./cursor-tool-names.js";
15
+ BUILTIN_NATIVE_CURSOR_TOOL_NAMES,
16
+ CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
17
+ CURSOR_REPLAY_TOOL_NAMES,
18
+ isNativeCursorToolName,
19
+ NATIVE_CURSOR_TOOL_NAMES,
20
+ type BuiltinNativeCursorToolName,
21
+ type NativeCursorToolName,
22
+ } from "./cursor-native-tool-names.js";
23
+ import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
18
24
  import {
19
25
  createCursorReplayOnlyToolDefinition,
20
26
  isCursorReplayNativeEditDetails,
@@ -29,8 +35,6 @@ import {
29
35
  isCursorReplayToolCallId,
30
36
  } from "./cursor-native-tool-display-state.js";
31
37
 
32
- const CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
33
- const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
34
38
 
35
39
  type AnyToolDefinition = ToolDefinition<TSchema, unknown, unknown>;
36
40
  type RenderCall = NonNullable<AnyToolDefinition["renderCall"]>;
@@ -117,8 +121,6 @@ function renderWriteReplayResult(
117
121
  : renderBase();
118
122
  }
119
123
 
120
- type BuiltinNativeCursorToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls";
121
-
122
124
  const NATIVE_CURSOR_TOOL_STRATEGIES: Record<BuiltinNativeCursorToolName, NativeReplayStrategy> = {
123
125
  read: {
124
126
  createDefinition: (cwd) => createReadToolDefinition(cwd) as AnyToolDefinition,
@@ -145,12 +147,6 @@ const NATIVE_CURSOR_TOOL_STRATEGIES: Record<BuiltinNativeCursorToolName, NativeR
145
147
  ls: { createDefinition: (cwd) => createLsToolDefinition(cwd) as AnyToolDefinition },
146
148
  };
147
149
 
148
- const BUILTIN_NATIVE_CURSOR_TOOL_NAMES = Object.keys(NATIVE_CURSOR_TOOL_STRATEGIES) as BuiltinNativeCursorToolName[];
149
- export const NATIVE_CURSOR_TOOL_NAMES = [
150
- ...BUILTIN_NATIVE_CURSOR_TOOL_NAMES,
151
- ...CURSOR_REPLAY_TOOL_NAMES,
152
- ] as readonly NativeCursorToolName[];
153
- export type NativeCursorToolName = BuiltinNativeCursorToolName | typeof CURSOR_REPLAY_TOOL_NAMES[number];
154
150
 
155
151
  function getNativeReplayStrategy(toolName: string): NativeReplayStrategy | undefined {
156
152
  return Object.hasOwn(NATIVE_CURSOR_TOOL_STRATEGIES, toolName)
@@ -158,9 +154,6 @@ function getNativeReplayStrategy(toolName: string): NativeReplayStrategy | undef
158
154
  : undefined;
159
155
  }
160
156
 
161
- export function isNativeCursorToolName(toolName: string): toolName is NativeCursorToolName {
162
- return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
163
- }
164
157
 
165
158
  export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
166
159
  definition: ToolDefinition<TParams, TDetails, TState>,
@@ -0,0 +1,16 @@
1
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-presentation-registry.js";
2
+
3
+ export const CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
4
+ export const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
5
+ export const BUILTIN_NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
6
+ export const NATIVE_CURSOR_TOOL_NAMES = [
7
+ ...BUILTIN_NATIVE_CURSOR_TOOL_NAMES,
8
+ ...CURSOR_REPLAY_TOOL_NAMES,
9
+ ] as readonly NativeCursorToolName[];
10
+
11
+ export type BuiltinNativeCursorToolName = typeof BUILTIN_NATIVE_CURSOR_TOOL_NAMES[number];
12
+ export type NativeCursorToolName = BuiltinNativeCursorToolName | typeof CURSOR_REPLAY_TOOL_NAMES[number];
13
+
14
+ export function isNativeCursorToolName(toolName: string): toolName is NativeCursorToolName {
15
+ return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
16
+ }
@@ -0,0 +1,12 @@
1
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+
3
+ export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
4
+ export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
5
+
6
+ export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | undefined> = process.env): boolean {
7
+ return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
8
+ }
9
+
10
+ export function resolveCursorPiToolBridgeBuiltinsEnabled(env: Record<string, string | undefined> = process.env): boolean {
11
+ return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV], false);
12
+ }
@@ -3,19 +3,17 @@ import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
3
3
  import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { buildCursorPiBridgeMcpToolDescription, CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX } from "./cursor-bridge-contract.js";
5
5
  import type { CursorPiBridgeToolDefinition, CursorPiMcpInputSchema } from "./cursor-pi-tool-bridge-types.js";
6
- import { getFirstStringByKeys } from "./cursor-record-utils.js";
7
-
8
- export function isRecord(value: unknown): value is Record<string, unknown> {
9
- return typeof value === "object" && value !== null;
10
- }
6
+ import { asRecord, stringifyUnknown } from "./cursor-record-utils.js";
11
7
 
12
8
  export function normalizeMcpInputSchema(schema: unknown): CursorPiMcpInputSchema {
13
- if (isRecord(schema) && schema.type === "object") return schema as CursorPiMcpInputSchema;
9
+ const record = asRecord(schema);
10
+ if (record?.type === "object") return record as CursorPiMcpInputSchema;
14
11
  return { type: "object", properties: {} };
15
12
  }
16
13
 
17
14
  export function normalizeMcpArgs(args: unknown): Record<string, unknown> {
18
- return isRecord(args) ? { ...args } : {};
15
+ const record = asRecord(args);
16
+ return record ? { ...record } : {};
19
17
  }
20
18
 
21
19
  export function waitForProtocolFlush(): Promise<void> {
@@ -73,21 +71,21 @@ export function snapshotToolToMcpTool(tool: CursorPiBridgeToolDefinition): Tool
73
71
 
74
72
  export function convertPiContentToMcpContent(content: unknown): CallToolResult["content"] {
75
73
  if (!Array.isArray(content)) {
76
- return [{ type: "text", text: typeof content === "string" ? content : JSON.stringify(content) }];
74
+ return [{ type: "text", text: stringifyUnknown(content) }];
77
75
  }
78
76
 
79
77
  const mcpContent: CallToolResult["content"] = [];
80
78
  for (const block of content) {
81
- if (!isRecord(block)) continue;
82
- if (block.type === "text" && typeof block.text === "string") {
83
- mcpContent.push({ type: "text", text: block.text });
79
+ const record = asRecord(block);
80
+ if (record?.type === "text" && typeof record.text === "string") {
81
+ mcpContent.push({ type: "text", text: record.text });
84
82
  continue;
85
83
  }
86
- if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string") {
87
- mcpContent.push({ type: "image", data: block.data, mimeType: block.mimeType });
84
+ if (record?.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") {
85
+ mcpContent.push({ type: "image", data: record.data, mimeType: record.mimeType });
88
86
  continue;
89
87
  }
90
- mcpContent.push({ type: "text", text: JSON.stringify(block) });
88
+ mcpContent.push({ type: "text", text: stringifyUnknown(block) });
91
89
  }
92
90
 
93
91
  return mcpContent.length > 0 ? mcpContent : [{ type: "text", text: "" }];
@@ -97,22 +95,19 @@ export function asToolResultMessage(value: Context["messages"][number]): ToolRes
97
95
  return value.role === "toolResult" ? value : undefined;
98
96
  }
99
97
 
100
- export function getStringField(record: Record<string, unknown>, fields: string[]): string | undefined {
101
- return getFirstStringByKeys(record, fields, { nonEmpty: true });
102
- }
103
-
104
98
  export function containsKnownMcpToolName(value: unknown, knownMcpToolNames: ReadonlySet<string>, depth = 0): boolean {
105
99
  if (depth > 4) return false;
106
100
  if (Array.isArray(value)) return value.some((entry) => containsKnownMcpToolName(entry, knownMcpToolNames, depth + 1));
107
- if (!isRecord(value)) return false;
101
+ const record = asRecord(value);
102
+ if (!record) return false;
108
103
 
109
104
  for (const field of ["tool", "toolName", "name", "mcpToolName", "serverToolName"]) {
110
- const fieldValue = value[field];
105
+ const fieldValue = record[field];
111
106
  if (typeof fieldValue === "string" && knownMcpToolNames.has(fieldValue)) return true;
112
107
  }
113
108
 
114
109
  for (const nestedField of ["args", "arguments", "input"]) {
115
- if (containsKnownMcpToolName(value[nestedField], knownMcpToolNames, depth + 1)) return true;
110
+ if (containsKnownMcpToolName(record[nestedField], knownMcpToolNames, depth + 1)) return true;
116
111
  }
117
112
 
118
113
  return false;
@@ -27,12 +27,11 @@ import {
27
27
  asToolResultMessage,
28
28
  containsKnownMcpToolName,
29
29
  convertPiContentToMcpContent,
30
- getStringField,
31
- isRecord,
32
30
  normalizeMcpArgs,
33
31
  snapshotToolToMcpTool,
34
32
  waitForProtocolFlush,
35
33
  } from "./cursor-pi-tool-bridge-mcp.js";
34
+ import { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
36
35
 
37
36
  export interface CursorPiToolBridgeRunHost {
38
37
  registerRun(pathname: string, run: CursorPiToolBridgeRunImpl): Promise<string>;
@@ -178,12 +177,13 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
178
177
  }
179
178
 
180
179
  isBridgeMcpToolCall(toolCall: unknown): boolean {
181
- if (!isRecord(toolCall)) return false;
182
- const toolName = getStringField(toolCall, ["name", "toolName", "mcpToolName"]);
180
+ const record = asRecord(toolCall);
181
+ if (!record) return false;
182
+ const toolName = getFirstStringByKeys(record, ["name", "toolName", "mcpToolName"], { nonEmpty: true });
183
183
  if (toolName && this.knownMcpToolNames.has(toolName)) return true;
184
184
 
185
185
  const isMcpEnvelope = toolName === "mcp" || toolName === MCP_SERVER_NAME;
186
- const cursorMcpCallId = getStringField(toolCall, ["call_id", "callId", "id", "toolCallId", "requestId"]);
186
+ const cursorMcpCallId = getFirstStringByKeys(record, ["call_id", "callId", "id", "toolCallId", "requestId"], { nonEmpty: true });
187
187
  if (cursorMcpCallId && this.knownCursorMcpCallIds.has(cursorMcpCallId) && isMcpEnvelope) return true;
188
188
 
189
189
  if (containsKnownMcpToolName(toolCall, this.knownMcpToolNames)) return true;
@@ -6,7 +6,7 @@ import type {
6
6
  CursorPiToolBridgeRunOptions,
7
7
  CursorPiToolBridgeSnapshotApi,
8
8
  } from "./cursor-pi-tool-bridge-types.js";
9
- import { isRecord } from "./cursor-pi-tool-bridge-mcp.js";
9
+ import { asRecord } from "./cursor-record-utils.js";
10
10
  import type { CursorPiToolBridgeRunImpl } from "./cursor-pi-tool-bridge-run.js";
11
11
  import {
12
12
  buildCursorPiToolBridgeSnapshot,
@@ -84,8 +84,13 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
84
84
  }
85
85
 
86
86
  getHttpServerAddress(): AddressInfo | undefined {
87
- const address = this.httpServer?.address();
88
- return isRecord(address) && typeof address.port === "number" ? address as AddressInfo : undefined;
87
+ const address = asRecord(this.httpServer?.address());
88
+ if (typeof address?.port !== "number") return undefined;
89
+ return {
90
+ address: typeof address.address === "string" ? address.address : LOOPBACK_HOST,
91
+ family: typeof address.family === "string" ? address.family : "IPv4",
92
+ port: address.port,
93
+ };
89
94
  }
90
95
 
91
96
  getEndpointCount(): number {
@@ -4,13 +4,15 @@ import type {
4
4
  CursorPiToolBridgeSnapshotApi,
5
5
  CursorPiToolBridgeSnapshotOptions,
6
6
  } from "./cursor-pi-tool-bridge-types.js";
7
- import { parseEnvBoolean } from "./cursor-env-boolean.js";
8
7
  import { createMcpToolName, normalizeMcpInputSchema, stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
8
+ export {
9
+ CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
+ CURSOR_PI_TOOL_BRIDGE_ENV,
11
+ resolveCursorPiToolBridgeBuiltinsEnabled,
12
+ resolveCursorPiToolBridgeEnabled,
13
+ } from "./cursor-pi-tool-bridge-env.js";
9
14
  import { isRegisteredCursorNativeToolName } from "./cursor-native-tool-display-state.js";
10
- import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-names.js";
11
-
12
- export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
13
- export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
15
+ import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-presentation-registry.js";
14
16
 
15
17
  const OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "write", "edit", "grep", "find", "ls"]);
16
18
 
@@ -22,14 +24,6 @@ export function createEmptySnapshot(): CursorPiToolBridgeSnapshot {
22
24
  };
23
25
  }
24
26
 
25
- export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | undefined> = process.env): boolean {
26
- return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
27
- }
28
-
29
- export function resolveCursorPiToolBridgeBuiltinsEnabled(env: Record<string, string | undefined> = process.env): boolean {
30
- return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV], false);
31
- }
32
-
33
27
  function isOverlappingCursorNativePiToolName(toolName: string): boolean {
34
28
  return OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES.has(toolName);
35
29
  }
@@ -8,11 +8,7 @@ import {
8
8
  import {
9
9
  CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
10
  CURSOR_PI_TOOL_BRIDGE_ENV,
11
- buildCursorPiToolBridgeSnapshot,
12
- buildCursorPiToolBridgeSurfaceSignature,
13
- resolveCursorPiToolBridgeBuiltinsEnabled,
14
- resolveCursorPiToolBridgeEnabled,
15
- } from "./cursor-pi-tool-bridge-snapshot.js";
11
+ } from "./cursor-pi-tool-bridge-env.js";
16
12
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
17
13
  import { MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
18
14
  import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
@@ -37,10 +33,14 @@ export type {
37
33
  export type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-diagnostics.js";
38
34
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
39
35
  export {
40
- buildCursorPiToolBridgeSnapshot,
41
- buildCursorPiToolBridgeSurfaceSignature,
36
+ CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
37
+ CURSOR_PI_TOOL_BRIDGE_ENV,
42
38
  resolveCursorPiToolBridgeBuiltinsEnabled,
43
39
  resolveCursorPiToolBridgeEnabled,
40
+ } from "./cursor-pi-tool-bridge-env.js";
41
+ export {
42
+ buildCursorPiToolBridgeSnapshot,
43
+ buildCursorPiToolBridgeSurfaceSignature,
44
44
  } from "./cursor-pi-tool-bridge-snapshot.js";
45
45
 
46
46
  let registeredCursorPiToolBridge: CursorPiToolBridgeRegistry | undefined;
@@ -8,10 +8,12 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
8
8
  "Cursor SDK request failed. The Cursor SDK API key may be missing, invalid, or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
9
9
  const AUTH_CURSOR_SDK_ERROR_MESSAGE =
10
10
  "Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
11
+ // Keep "Network error" aligned with pi's agent-level retry classifier.
11
12
  const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
12
- "Cursor SDK request failed during network or service I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
13
+ "Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
13
14
 
14
- const GENERIC_CURSOR_RUN_FAILURE_TEXT = "cursor sdk run failed";
15
+ // Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
16
+ const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
15
17
 
16
18
  export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
17
19
 
@@ -20,9 +22,13 @@ function isGenericErrorMessage(message: string): boolean {
20
22
  return normalized === "" || normalized === "error" || normalized === "unknown error";
21
23
  }
22
24
 
25
+ function isGenericCursorRunFailureMessage(message: string): boolean {
26
+ return /^cursor sdk run failed\.?$/i.test(message.trim());
27
+ }
28
+
23
29
  function isKnownGenericRunFailureText(message: string): boolean {
24
30
  const normalized = message.trim().toLowerCase();
25
- return normalized === "" || normalized === GENERIC_CURSOR_RUN_FAILURE_TEXT || isGenericErrorMessage(normalized);
31
+ return normalized === "" || isGenericCursorRunFailureMessage(message) || isGenericErrorMessage(normalized);
26
32
  }
27
33
 
28
34
  function isLikelyAuthError(message: string): boolean {
@@ -150,7 +156,7 @@ export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSourc
150
156
  return fromRun;
151
157
  }
152
158
 
153
- const parts = ["Cursor SDK run failed"];
159
+ const parts = [RETRYABLE_CURSOR_RUN_FAILURE_PREFIX];
154
160
  if (result.model?.id) parts.push(`model ${result.model.id}`);
155
161
  parts.push(`run ${shortRunId(result.id)}`);
156
162
  if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
@@ -190,6 +196,7 @@ export function sanitizeCursorProviderError(error: unknown, apiKey?: string): st
190
196
  const connectClassification = classifyCursorConnectError(error);
191
197
  if (connectClassification?.kind === "unauthenticated" || isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
192
198
  if (connectClassification?.kind === "network" || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
199
+ if (isGenericCursorRunFailureMessage(scrubbed)) return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
193
200
  if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
194
201
  return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
195
202
  }
@@ -0,0 +1,51 @@
1
+ import {
2
+ createAssistantMessageEventStream,
3
+ type Api,
4
+ type AssistantMessage,
5
+ type AssistantMessageEventStream,
6
+ type Context,
7
+ type Model,
8
+ type SimpleStreamOptions,
9
+ } from "@earendil-works/pi-ai";
10
+
11
+ function makeProviderLoadErrorMessage(model: Model<Api>, error: unknown): AssistantMessage {
12
+ return {
13
+ role: "assistant",
14
+ content: [],
15
+ api: model.api,
16
+ provider: model.provider,
17
+ model: model.id,
18
+ usage: {
19
+ input: 0,
20
+ output: 0,
21
+ cacheRead: 0,
22
+ cacheWrite: 0,
23
+ totalTokens: 0,
24
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
25
+ },
26
+ stopReason: "error",
27
+ timestamp: Date.now(),
28
+ errorMessage: `Failed to load Cursor provider runtime: ${error instanceof Error ? error.message : String(error)}`,
29
+ };
30
+ }
31
+
32
+ export function streamCursorLazy(
33
+ model: Model<Api>,
34
+ context: Context,
35
+ options?: SimpleStreamOptions,
36
+ ): AssistantMessageEventStream {
37
+ const outer = createAssistantMessageEventStream();
38
+ queueMicrotask(async () => {
39
+ try {
40
+ const { streamCursor } = await import("./cursor-provider.js");
41
+ for await (const event of streamCursor(model, context, options)) {
42
+ outer.push(event);
43
+ }
44
+ } catch (error) {
45
+ const message = makeProviderLoadErrorMessage(model, error);
46
+ outer.push({ type: "error", reason: "error", error: message });
47
+ outer.end(message);
48
+ }
49
+ });
50
+ return outer;
51
+ }
@@ -17,7 +17,7 @@ import {
17
17
  deleteCursorNativeToolDisplay,
18
18
  recordCursorNativeToolDisplay,
19
19
  type CursorNativeToolDisplayItem,
20
- } from "./cursor-native-tool-display.js";
20
+ } from "./cursor-native-tool-display-state.js";
21
21
  import { type CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
22
22
  import { resetSessionCursorAgent } from "./cursor-session-agent.js";
23
23
  import { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
@@ -1,6 +1,5 @@
1
1
  import type { AssistantMessage } from "@earendil-works/pi-ai";
2
- import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
3
- import { abandonSessionCursorAgent } from "./cursor-provider-live-run-drain.js";
2
+ import { abandonSessionCursorAgent, cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
4
3
  import {
5
4
  classifyCursorRunEmission,
6
5
  getCursorRunAbortMessage,
@@ -12,7 +11,10 @@ import {
12
11
  sanitizeCursorProviderError,
13
12
  } from "./cursor-provider-errors.js";
14
13
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
15
- import type { IncompleteCursorToolRunOutcomeInput } from "./cursor-incomplete-tool-visibility.js";
14
+ import {
15
+ buildIncompleteCursorToolRunOutcome,
16
+ type IncompleteCursorToolRunOutcomeInput,
17
+ } from "./cursor-incomplete-tool-visibility.js";
16
18
  import type { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
17
19
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
18
20
  import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
@@ -24,8 +26,6 @@ import type {
24
26
  } from "./cursor-provider-turn-types.js";
25
27
  import { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
26
28
  import { hasUsableText } from "./cursor-record-utils.js";
27
- import { buildIncompleteCursorToolRunOutcome } from "./cursor-incomplete-tool-visibility.js";
28
-
29
29
  export type CursorTurnTerminalEvent =
30
30
  | {
31
31
  kind: "direct";
@@ -10,7 +10,6 @@ import { hasUsableText } from "./cursor-record-utils.js";
10
10
  import {
11
11
  buildIncompleteCursorToolRunOutcome,
12
12
  type IncompleteCursorToolRunOutcome,
13
- type IncompleteCursorToolRunOutcomeInput,
14
13
  } from "./cursor-incomplete-tool-visibility.js";
15
14
 
16
15
  /** Unified SDK wait() facts consumed by live and direct emission strategies. */