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
@@ -2,19 +2,15 @@ import type { Dirent } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join, relative } from "node:path";
4
4
  import type {
5
- BeforeAgentStartEvent,
6
- BeforeAgentStartEventResult,
7
5
  BuildSystemPromptOptions,
8
6
  ExtensionAPI,
9
7
  ExtensionContext,
10
- ExtensionHandler,
11
- SessionStartEvent,
12
8
  Skill,
13
- TurnStartEvent,
14
9
  } from "@earendil-works/pi-coding-agent";
15
10
  import { Type } from "typebox";
16
11
  import { isCursorModel } from "./cursor-model.js";
17
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-snapshot.js";
12
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
13
+ import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
18
14
 
19
15
  export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
20
16
  export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
@@ -23,12 +19,7 @@ const AVAILABLE_SKILLS_SECTION_PATTERN = /\n\nThe following skills provide speci
23
19
  const MAX_SKILL_RESOURCES = 80;
24
20
  const RESOURCE_DIR_NAMES = ["scripts", "references", "assets"] as const;
25
21
 
26
- type CursorSkillToolExtensionApi = Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> & {
27
- on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
28
- on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
29
- on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
30
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
31
- };
22
+ type CursorSkillToolExtensionApi = Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> & CursorModelLifecycleExtensionApi;
32
23
 
33
24
  type CursorActivateSkillParams = {
34
25
  name?: string;
@@ -229,26 +220,28 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
229
220
  syncCursorSkillToolForModel(pi, model);
230
221
  };
231
222
 
232
- pi.on("session_start", (_event, ctx) => {
233
- clearSkillsAndSync(ctx.model);
234
- });
235
- pi.on("model_select", (event) => {
236
- clearSkillsAndSync(event.model);
237
- });
238
- pi.on("turn_start", (_event, ctx) => {
239
- if (!isCursorModel(ctx.model)) setCurrentSkills([]);
240
- syncCursorSkillToolForModel(pi, ctx.model);
241
- });
242
- pi.on("before_agent_start", (event, ctx) => {
243
- if (isCursorModel(ctx.model)) {
244
- setCurrentSkills(event.systemPromptOptions?.skills);
245
- } else {
246
- setCurrentSkills([]);
247
- }
248
- syncCursorSkillToolForModel(pi, ctx.model);
249
- const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
250
- if (resolved === event.systemPrompt) return undefined;
251
- return { systemPrompt: resolved };
223
+ registerCursorModelLifecycle(pi, {
224
+ sessionStart: (_event, ctx) => {
225
+ clearSkillsAndSync(ctx.model);
226
+ },
227
+ modelSelect: (event) => {
228
+ clearSkillsAndSync(event.model);
229
+ },
230
+ turnStart: (_event, ctx) => {
231
+ if (!isCursorModel(ctx.model)) setCurrentSkills([]);
232
+ syncCursorSkillToolForModel(pi, ctx.model);
233
+ },
234
+ beforeAgentStart: (event, ctx) => {
235
+ if (isCursorModel(ctx.model)) {
236
+ setCurrentSkills(event.systemPromptOptions?.skills);
237
+ } else {
238
+ setCurrentSkills([]);
239
+ }
240
+ syncCursorSkillToolForModel(pi, ctx.model);
241
+ const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
242
+ if (resolved === event.systemPrompt) return undefined;
243
+ return { systemPrompt: resolved };
244
+ },
252
245
  });
253
246
  }
254
247
 
@@ -15,9 +15,12 @@ import {
15
15
  } from "./cursor-pi-tool-bridge-snapshot.js";
16
16
  import {
17
17
  CURSOR_SETTING_SOURCES_ENV,
18
- getEffectiveCursorSettingSources,
18
+ resolveCursorSettingSources,
19
19
  } from "./cursor-setting-sources.js";
20
20
  import { isCursorModel } from "./cursor-model.js";
21
+ import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
22
+ import { asRecord } from "./cursor-record-utils.js";
23
+ import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
21
24
  import { getCursorModelMetadata } from "./model-discovery.js";
22
25
 
23
26
  const FAST_ENTRY_TYPE = "cursor-fast-state";
@@ -58,6 +61,7 @@ let cliForceFast = false;
58
61
  let cliForceNoFast = false;
59
62
  let sessionCursorAgentMode: AgentModeOption | undefined;
60
63
  let cliCursorModeState: CursorCliModeState = { kind: "unset" };
64
+ const invalidCursorModeNotifiedSessionScopeKeys = new Set<string>();
61
65
 
62
66
  export function isCursorAgentMode(value: unknown): value is AgentModeOption {
63
67
  return value === "agent" || value === "plan";
@@ -69,13 +73,10 @@ export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined
69
73
  return isCursorAgentMode(mode) ? mode : undefined;
70
74
  }
71
75
 
72
- function isRecord(value: unknown): value is Record<string, unknown> {
73
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
74
- }
75
-
76
76
  function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
77
- if (!isRecord(value)) return false;
78
- return (typeof value.modelId === "string" || typeof value.baseModelId === "string") && typeof value.fast === "boolean";
77
+ const record = asRecord(value);
78
+ if (!record) return false;
79
+ return (typeof record.modelId === "string" || typeof record.baseModelId === "string") && typeof record.fast === "boolean";
79
80
  }
80
81
 
81
82
  function getCursorFastEntryModelId(data: CursorFastEntryData): string {
@@ -83,17 +84,19 @@ function getCursorFastEntryModelId(data: CursorFastEntryData): string {
83
84
  }
84
85
 
85
86
  function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
86
- return isRecord(value) && isCursorAgentMode(value.mode);
87
+ return isCursorAgentMode(asRecord(value)?.mode);
87
88
  }
88
89
 
89
90
  function parseCursorGlobalConfig(value: unknown): CursorGlobalConfig | undefined {
90
- if (!isRecord(value)) return undefined;
91
- const { fastDefaults } = value;
91
+ const record = asRecord(value);
92
+ if (!record) return undefined;
93
+ const { fastDefaults } = record;
92
94
  if (fastDefaults === undefined) return {};
93
- if (!isRecord(fastDefaults)) return undefined;
95
+ const fastDefaultsRecord = asRecord(fastDefaults);
96
+ if (!fastDefaultsRecord) return undefined;
94
97
  return {
95
98
  fastDefaults: Object.fromEntries(
96
- Object.entries(fastDefaults).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean"),
99
+ Object.entries(fastDefaultsRecord).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean"),
97
100
  ),
98
101
  };
99
102
  }
@@ -174,23 +177,38 @@ function formatInvalidCursorMode(raw: string): string {
174
177
  return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`;
175
178
  }
176
179
 
177
- export function getEffectiveCursorAgentMode(): AgentModeOption {
180
+ export type CursorAgentModeResolution =
181
+ | { kind: "valid"; mode: AgentModeOption }
182
+ | { kind: "invalid"; raw: string; message: string };
183
+
184
+ export function getStoredCursorAgentMode(): AgentModeOption {
185
+ return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
186
+ }
187
+
188
+ export function resolveCursorAgentMode(): CursorAgentModeResolution {
178
189
  switch (cliCursorModeState.kind) {
179
190
  case "valid":
180
- return cliCursorModeState.mode;
191
+ return { kind: "valid", mode: cliCursorModeState.mode };
181
192
  case "invalid":
182
- throw new Error(cliCursorModeState.message);
193
+ return { kind: "invalid", raw: cliCursorModeState.raw, message: cliCursorModeState.message };
183
194
  case "unset":
184
- return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
195
+ return { kind: "valid", mode: getStoredCursorAgentMode() };
185
196
  }
186
197
  }
187
198
 
199
+ export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
200
+ const resolution = resolveCursorAgentMode();
201
+ if (resolution.kind === "invalid") throw new Error(resolution.message);
202
+ return resolution.mode;
203
+ }
204
+
188
205
  function formatCursorStatus(fast: boolean | undefined): string | undefined {
189
206
  const parts: string[] = [];
207
+ const modeResolution = resolveCursorAgentMode();
190
208
  if (fast === true) parts.push("fast");
191
- if (cliCursorModeState.kind === "invalid") {
209
+ if (modeResolution.kind === "invalid") {
192
210
  parts.push("mode invalid");
193
- } else if (getEffectiveCursorAgentMode() === "plan") {
211
+ } else if (modeResolution.mode === "plan") {
194
212
  parts.push("plan");
195
213
  }
196
214
  return parts.length > 0 ? `cursor ${parts.join(" · ")}` : undefined;
@@ -255,7 +273,7 @@ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode
255
273
  }
256
274
  }
257
275
 
258
- function restoreCliCursorMode(raw: boolean | string | undefined, mode: ExtensionContext["mode"], notify: ExtensionContext["ui"]["notify"]): void {
276
+ function restoreCliCursorMode(raw: boolean | string | undefined): void {
259
277
  cliCursorModeState = { kind: "unset" };
260
278
  if (raw === undefined || raw === "" || raw === false) return;
261
279
  const parsed = parseCursorAgentMode(raw);
@@ -266,15 +284,19 @@ function restoreCliCursorMode(raw: boolean | string | undefined, mode: Extension
266
284
  const rawText = String(raw);
267
285
  const message = formatInvalidCursorMode(rawText);
268
286
  cliCursorModeState = { kind: "invalid", raw: rawText, message };
269
- if (mode === "tui") {
270
- notify(message, "error");
271
- return;
272
- }
273
- throw new Error(message);
287
+ }
288
+
289
+ function notifyInvalidCursorModeIfCursorActive(ctx: Pick<ExtensionContext, "hasUI" | "mode" | "ui">): void {
290
+ const modeResolution = resolveCursorAgentMode();
291
+ if (modeResolution.kind !== "invalid" || !ctx.hasUI || ctx.mode !== "tui") return;
292
+ const scopeKey = getCursorSessionScopeKey();
293
+ if (invalidCursorModeNotifiedSessionScopeKeys.has(scopeKey)) return;
294
+ invalidCursorModeNotifiedSessionScopeKeys.add(scopeKey);
295
+ ctx.ui.notify(modeResolution.message, "error");
274
296
  }
275
297
 
276
298
  function formatEffectiveCursorSettingSourcesLabel(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): string {
277
- const effective = getEffectiveCursorSettingSources(raw);
299
+ const effective = resolveCursorSettingSources(raw);
278
300
  const effectiveLabel = effective === undefined ? "none" : effective.join(",");
279
301
  const rawLabel = raw?.trim() ? raw.trim() : "(unset → all)";
280
302
  return `${rawLabel} (effective: ${effectiveLabel})`;
@@ -395,10 +417,11 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
395
417
  const usage = "Usage: /cursor-mode agent|plan";
396
418
  const mode = parseCursorAgentMode(args);
397
419
  if (!args.trim()) {
398
- try {
399
- ctx.ui.notify(`Cursor mode is ${getEffectiveCursorAgentMode()}. ${usage}`, "info");
400
- } catch (error) {
401
- ctx.ui.notify(`${error instanceof Error ? error.message : String(error)} ${usage}`, "error");
420
+ const modeResolution = resolveCursorAgentMode();
421
+ if (modeResolution.kind === "invalid") {
422
+ ctx.ui.notify(`${modeResolution.message} ${usage}`, "error");
423
+ } else {
424
+ ctx.ui.notify(`Cursor mode is ${modeResolution.mode}. ${usage}`, "info");
402
425
  }
403
426
  return;
404
427
  }
@@ -429,28 +452,26 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
429
452
  },
430
453
  });
431
454
 
432
- pi.on("session_start", async (_event, ctx) => {
433
- globalFastPreferences = loadGlobalFastPreferences();
434
- cliForceFast = pi.getFlag("cursor-fast") === true;
435
- cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
436
- restoreSessionFastPreferences(ctx);
437
- restoreSessionCursorMode(ctx);
438
- restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.mode, ctx.ui.notify.bind(ctx.ui));
439
- updateCursorStatus(ctx);
440
- });
441
-
442
- pi.on("model_select", async (event, ctx) => {
443
- updateCursorStatus(ctx, event.model);
444
- });
445
-
446
- pi.on("turn_start", async (_event, ctx) => {
447
- updateCursorStatus(ctx);
455
+ registerCursorModelLifecycle(pi, {
456
+ sessionStart: (_event, ctx) => {
457
+ globalFastPreferences = loadGlobalFastPreferences();
458
+ cliForceFast = pi.getFlag("cursor-fast") === true;
459
+ cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
460
+ restoreSessionFastPreferences(ctx);
461
+ restoreSessionCursorMode(ctx);
462
+ restoreCliCursorMode(pi.getFlag("cursor-mode"));
463
+ },
464
+ sync: (ctx) => {
465
+ if (isCursorModel(ctx.model)) notifyInvalidCursorModeIfCursorActive(ctx);
466
+ updateCursorStatus(ctx);
467
+ },
448
468
  });
449
469
  }
450
470
 
451
471
  function resetCursorModeStateForTests(): void {
452
472
  sessionCursorAgentMode = undefined;
453
473
  cliCursorModeState = { kind: "unset" };
474
+ invalidCursorModeNotifiedSessionScopeKeys.clear();
454
475
  }
455
476
 
456
477
  export const __testUtils = {
@@ -1,9 +1,10 @@
1
1
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
2
2
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
3
  import { getCursorToolLifecycleLabelKind } from "./cursor-tool-presentation-registry.js";
4
- import { extractWebSearchQuery } from "./cursor-web-tool-activity.js";
5
- import { firstNonEmptyLine, getArray, getString, truncateArg } from "./cursor-transcript-utils.js";
6
- import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
4
+ import { extractWebSearchQuery } from "./cursor-web-tool-args.js";
5
+ import { getArray, getString } from "./cursor-record-utils.js";
6
+ import { firstNonEmptyLine, truncateArg } from "./cursor-transcript-utils.js";
7
+ import { classifyCursorToolVisibility, type CursorToolVisibility } from "./cursor-tool-visibility.js";
7
8
 
8
9
  /** Defer pending lifecycle lines so fast start+complete pairs coalesce into the completed replay card only. */
9
10
  export const CURSOR_TOOL_LIFECYCLE_DEFER_MS = 75;
@@ -12,8 +13,7 @@ export function isCursorToolLifecycleEligible(toolCall: unknown): boolean {
12
13
  return classifyCursorToolVisibility(toolCall).lifecycleEligible;
13
14
  }
14
15
 
15
- function getCursorToolLifecycleTitle(toolCall: unknown): string {
16
- const visibility = classifyCursorToolVisibility(toolCall);
16
+ function getCursorToolLifecycleTitle(visibility: CursorToolVisibility): string {
17
17
  return visibility.lifecycleTitle ?? `Cursor ${visibility.normalizedName}`;
18
18
  }
19
19
 
@@ -36,8 +36,10 @@ function scrubLifecycleDetail(value: string | undefined, apiKey?: string): strin
36
36
  return scrubbed;
37
37
  }
38
38
 
39
- export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
40
- const visibility = classifyCursorToolVisibility(toolCall);
39
+ function buildCursorToolLifecycleLabelFromVisibility(
40
+ visibility: CursorToolVisibility,
41
+ apiKey?: string,
42
+ ): string | undefined {
41
43
  const args = visibility.args;
42
44
 
43
45
  switch (getCursorToolLifecycleLabelKind(visibility.normalizedKey)) {
@@ -79,8 +81,13 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
79
81
  }
80
82
  }
81
83
 
84
+ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
85
+ return buildCursorToolLifecycleLabelFromVisibility(classifyCursorToolVisibility(toolCall), apiKey);
86
+ }
87
+
82
88
  export function formatCursorToolLifecycleProgressText(toolCall: unknown, apiKey?: string): string | undefined {
83
- const label = buildCursorToolLifecycleLabel(toolCall, apiKey);
89
+ const visibility = classifyCursorToolVisibility(toolCall);
90
+ const label = buildCursorToolLifecycleLabelFromVisibility(visibility, apiKey);
84
91
  if (!label) return undefined;
85
- return `${getCursorToolLifecycleTitle(toolCall)}: ${label}\n`;
92
+ return `${getCursorToolLifecycleTitle(visibility)}: ${label}\n`;
86
93
  }