pi-cursor-sdk 0.1.20 → 0.1.22

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 (89) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +49 -9
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +115 -9
  5. package/docs/cursor-model-ux-spec.md +58 -18
  6. package/docs/cursor-native-tool-replay.md +15 -7
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +8 -3
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +34 -10
  11. package/scripts/debug-provider-events.d.mts +59 -0
  12. package/scripts/debug-provider-events.mjs +70 -175
  13. package/scripts/debug-sdk-events.d.mts +90 -0
  14. package/scripts/debug-sdk-events.mjs +36 -98
  15. package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
  16. package/scripts/isolated-cursor-smoke.sh +264 -102
  17. package/scripts/lib/cursor-child-process.d.mts +10 -0
  18. package/scripts/lib/cursor-child-process.mjs +50 -0
  19. package/scripts/lib/cursor-cli-args.d.mts +63 -0
  20. package/scripts/lib/cursor-cli-args.mjs +129 -0
  21. package/scripts/lib/cursor-script-fail.d.mts +1 -0
  22. package/scripts/lib/cursor-script-fail.mjs +13 -0
  23. package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
  24. package/scripts/lib/cursor-smoke-env.d.mts +38 -0
  25. package/scripts/lib/cursor-smoke-env.mjs +81 -0
  26. package/scripts/lib/cursor-smoke-shell.sh +174 -0
  27. package/scripts/lib/cursor-visual-render.d.mts +15 -0
  28. package/scripts/lib/cursor-visual-render.mjs +131 -0
  29. package/scripts/probe-mcp-coldstart.mjs +20 -38
  30. package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
  31. package/scripts/steering-rpc-smoke.mjs +170 -65
  32. package/scripts/tmux-live-smoke.sh +152 -98
  33. package/scripts/visual-tui-smoke.mjs +659 -0
  34. package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
  35. package/shared/cursor-sdk-event-debug-env.mjs +13 -0
  36. package/shared/cursor-sensitive-text.d.mts +1 -0
  37. package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
  38. package/shared/cursor-setting-sources.d.mts +5 -0
  39. package/shared/cursor-setting-sources.mjs +22 -0
  40. package/src/context.ts +21 -12
  41. package/src/cursor-bridge-contract.ts +1 -3
  42. package/src/cursor-incomplete-tool-visibility.ts +22 -5
  43. package/src/cursor-native-tool-display-registration.ts +63 -27
  44. package/src/cursor-native-tool-display-replay.ts +246 -144
  45. package/src/cursor-native-tool-display-state.ts +2 -0
  46. package/src/cursor-native-tool-display-tools.ts +149 -41
  47. package/src/cursor-provider-live-run-drain.ts +1 -52
  48. package/src/cursor-provider-run-finalizer.ts +237 -0
  49. package/src/cursor-provider-run-outcome.ts +149 -0
  50. package/src/cursor-provider-turn-api-key.ts +8 -0
  51. package/src/cursor-provider-turn-coordinator.ts +98 -446
  52. package/src/cursor-provider-turn-display-router.ts +216 -0
  53. package/src/cursor-provider-turn-emit.ts +59 -0
  54. package/src/cursor-provider-turn-finalize.ts +119 -0
  55. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  56. package/src/cursor-provider-turn-message-offset.ts +15 -0
  57. package/src/cursor-provider-turn-prepare.ts +216 -0
  58. package/src/cursor-provider-turn-runner.ts +140 -0
  59. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  60. package/src/cursor-provider-turn-send.ts +103 -0
  61. package/src/cursor-provider-turn-shell-output.ts +107 -0
  62. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  63. package/src/cursor-provider-turn-types.ts +87 -0
  64. package/src/cursor-provider.ts +16 -504
  65. package/src/cursor-replay-activity-builders.ts +276 -0
  66. package/src/cursor-replay-source-names.ts +33 -0
  67. package/src/cursor-replay-summary-args.ts +191 -0
  68. package/src/cursor-replay-tool-details.ts +464 -0
  69. package/src/cursor-run-final-text.ts +56 -0
  70. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  71. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  72. package/src/cursor-sdk-event-debug.ts +2 -1
  73. package/src/cursor-sensitive-text.ts +3 -36
  74. package/src/cursor-session-agent.ts +3 -1
  75. package/src/cursor-session-compaction-prep.ts +19 -0
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +9 -8
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -106
  81. package/src/cursor-tool-presentation-registry.ts +556 -0
  82. package/src/cursor-tool-transcript.ts +1 -1
  83. package/src/cursor-tool-visibility.ts +3 -27
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +158 -233
  86. package/src/cursor-transcript-utils.ts +0 -44
  87. package/src/cursor-web-tool-activity.ts +10 -60
  88. package/src/cursor-web-tool-args.ts +39 -0
  89. package/src/index.ts +8 -10
@@ -1,17 +1,14 @@
1
1
  import type { SettingSource } from "@cursor/sdk";
2
+ /** Provider-facing wrapper; canonical parsing lives in shared/cursor-setting-sources.mjs. */
3
+ import {
4
+ CURSOR_SETTING_SOURCES_ENV as CURSOR_SETTING_SOURCES_ENV_JS,
5
+ resolveCursorSettingSources as resolveCursorSettingSourcesJs,
6
+ } from "../shared/cursor-setting-sources.mjs";
2
7
 
3
- export const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
8
+ export const CURSOR_SETTING_SOURCES_ENV = CURSOR_SETTING_SOURCES_ENV_JS;
4
9
 
5
10
  export function resolveCursorSettingSources(raw?: string): SettingSource[] | undefined {
6
- const trimmed = raw?.trim();
7
- if (!trimmed) return ["all"];
8
- const normalized = trimmed.toLowerCase();
9
- if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
10
- if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
11
- return trimmed
12
- .split(",")
13
- .map((entry) => entry.trim())
14
- .filter((entry): entry is SettingSource => Boolean(entry));
11
+ return resolveCursorSettingSourcesJs(raw) as SettingSource[] | undefined;
15
12
  }
16
13
 
17
14
  export function getEffectiveCursorSettingSources(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): SettingSource[] | undefined {
@@ -1,46 +1,72 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
3
+ import type { AgentModeOption } from "@cursor/sdk";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ buildCursorToolManifestText,
8
+ CURSOR_TOOL_MANIFEST_ENV,
9
+ resolveCursorToolManifestEnabled,
10
+ } from "./cursor-tool-manifest.js";
11
+ import {
12
+ buildCursorPiToolBridgeSnapshot,
13
+ CURSOR_PI_TOOL_BRIDGE_ENV,
14
+ resolveCursorPiToolBridgeEnabled,
15
+ } from "./cursor-pi-tool-bridge-snapshot.js";
16
+ import {
17
+ CURSOR_SETTING_SOURCES_ENV,
18
+ getEffectiveCursorSettingSources,
19
+ } from "./cursor-setting-sources.js";
5
20
  import { isCursorModel } from "./cursor-model.js";
6
21
  import { getCursorModelMetadata } from "./model-discovery.js";
7
22
 
8
23
  const FAST_ENTRY_TYPE = "cursor-fast-state";
24
+ const MODE_ENTRY_TYPE = "cursor-mode-state";
9
25
  const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
10
26
 
27
+ export type CursorAgentMode = AgentModeOption;
28
+
29
+ const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent";
30
+
11
31
  interface CursorFastEntryData {
12
32
  baseModelId: string;
13
33
  fast: boolean;
14
34
  }
15
35
 
36
+ interface CursorModeEntryData {
37
+ mode: AgentModeOption;
38
+ }
39
+
16
40
  interface CursorGlobalConfig {
17
41
  fastDefaults?: Record<string, boolean>;
18
42
  }
19
43
 
20
- type CursorFastControlsModel =
21
- | Pick<NonNullable<ExtensionContext["model"]>, "id" | "provider" | "api">
22
- | undefined;
23
-
24
- type CursorFastControlsContext = {
25
- model: CursorFastControlsModel;
26
- ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
27
- sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch">;
28
- };
44
+ type CursorRuntimeControlsExtensionApi = Pick<
45
+ ExtensionAPI,
46
+ "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools"
47
+ >;
29
48
 
30
- interface CursorFastControlsExtensionApi extends Pick<ExtensionAPI, "appendEntry" | "getFlag" | "registerFlag"> {
31
- registerCommand(name: string, options: {
32
- description?: string;
33
- handler: (args: string, ctx: CursorFastControlsContext) => Promise<void> | void;
34
- }): void;
35
- on(event: "session_start", handler: (event: SessionStartEvent, ctx: CursorFastControlsContext) => Promise<void> | void): void;
36
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: CursorFastControlsContext) => Promise<void> | void): void;
37
- on(event: "turn_start", handler: (event: unknown, ctx: CursorFastControlsContext) => Promise<void> | void): void;
38
- }
49
+ type CursorCliModeState =
50
+ | { kind: "unset" }
51
+ | { kind: "valid"; mode: AgentModeOption }
52
+ | { kind: "invalid"; raw: string; message: string };
39
53
 
40
54
  const sessionFastPreferences = new Map<string, boolean>();
41
55
  let globalFastPreferences = new Map<string, boolean>();
42
56
  let cliForceFast = false;
43
57
  let cliForceNoFast = false;
58
+ let sessionCursorAgentMode: AgentModeOption | undefined;
59
+ let cliCursorModeState: CursorCliModeState = { kind: "unset" };
60
+
61
+ export function isCursorAgentMode(value: unknown): value is AgentModeOption {
62
+ return value === "agent" || value === "plan";
63
+ }
64
+
65
+ export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined {
66
+ if (typeof raw !== "string") return undefined;
67
+ const mode = raw.trim();
68
+ return isCursorAgentMode(mode) ? mode : undefined;
69
+ }
44
70
 
45
71
  function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
46
72
  if (!value || typeof value !== "object") return false;
@@ -48,6 +74,12 @@ function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
48
74
  return typeof data.baseModelId === "string" && typeof data.fast === "boolean";
49
75
  }
50
76
 
77
+ function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
78
+ if (!value || typeof value !== "object") return false;
79
+ const data = value as Record<string, unknown>;
80
+ return isCursorAgentMode(data.mode);
81
+ }
82
+
51
83
  function getConfigPath(): string {
52
84
  return join(getAgentDir(), GLOBAL_CONFIG_FILE);
53
85
  }
@@ -86,6 +118,16 @@ function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionCont
86
118
  }
87
119
  }
88
120
 
121
+ function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
122
+ sessionCursorAgentMode = undefined;
123
+ for (const entry of ctx.sessionManager.getBranch()) {
124
+ if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE) continue;
125
+ if (isCursorModeEntryData(entry.data)) {
126
+ sessionCursorAgentMode = entry.data.mode;
127
+ }
128
+ }
129
+ }
130
+
89
131
  function getEffectiveFast(baseModelId: string, modelId: string): boolean | undefined {
90
132
  const metadata = getCursorModelMetadata(modelId);
91
133
  if (!metadata?.supportsFast) return undefined;
@@ -94,21 +136,43 @@ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undef
94
136
  return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
95
137
  }
96
138
 
97
- function updateCursorStatus(ctx: { model: CursorFastControlsModel; ui: Pick<ExtensionContext["ui"], "setStatus"> }, model = ctx.model): void {
139
+ function formatInvalidCursorMode(raw: string): string {
140
+ return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`;
141
+ }
142
+
143
+ export function getEffectiveCursorAgentMode(): AgentModeOption {
144
+ switch (cliCursorModeState.kind) {
145
+ case "valid":
146
+ return cliCursorModeState.mode;
147
+ case "invalid":
148
+ throw new Error(cliCursorModeState.message);
149
+ case "unset":
150
+ return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
151
+ }
152
+ }
153
+
154
+ function formatCursorStatus(fast: boolean | undefined): string | undefined {
155
+ const parts: string[] = [];
156
+ if (fast === true) parts.push("fast");
157
+ if (cliCursorModeState.kind === "invalid") {
158
+ parts.push("mode invalid");
159
+ } else if (getEffectiveCursorAgentMode() === "plan") {
160
+ parts.push("plan");
161
+ }
162
+ return parts.length > 0 ? `cursor ${parts.join(" · ")}` : undefined;
163
+ }
164
+
165
+ function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
98
166
  if (!model || !isCursorModel(model)) {
99
167
  ctx.ui.setStatus("cursor", undefined);
100
168
  return;
101
169
  }
102
170
  const metadata = getCursorModelMetadata(model.id);
103
- if (!metadata?.supportsFast) {
104
- ctx.ui.setStatus("cursor", undefined);
105
- return;
106
- }
107
- const fast = getEffectiveFast(metadata.baseModelId, model.id);
108
- ctx.ui.setStatus("cursor", fast === true ? "cursor fast" : undefined);
171
+ const fast = metadata?.supportsFast ? getEffectiveFast(metadata.baseModelId, model.id) : undefined;
172
+ ctx.ui.setStatus("cursor", formatCursorStatus(fast));
109
173
  }
110
174
 
111
- function getCurrentCursorMetadata(ctx: { model: CursorFastControlsModel }) {
175
+ function getCurrentCursorMetadata(ctx: Pick<ExtensionContext, "model">) {
112
176
  const model = ctx.model;
113
177
  if (!model || !isCursorModel(model)) return undefined;
114
178
  return getCursorModelMetadata(model.id);
@@ -146,13 +210,87 @@ function persistFastPreference(pi: Pick<ExtensionAPI, "appendEntry">, baseModelI
146
210
  }
147
211
  }
148
212
 
213
+ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode: AgentModeOption): void {
214
+ const previousMode = sessionCursorAgentMode;
215
+ sessionCursorAgentMode = mode;
216
+ try {
217
+ pi.appendEntry<CursorModeEntryData>(MODE_ENTRY_TYPE, { mode });
218
+ } catch (error) {
219
+ sessionCursorAgentMode = previousMode;
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean, notify: ExtensionContext["ui"]["notify"]): void {
225
+ cliCursorModeState = { kind: "unset" };
226
+ if (raw === undefined || raw === "" || raw === false) return;
227
+ const parsed = parseCursorAgentMode(raw);
228
+ if (parsed) {
229
+ cliCursorModeState = { kind: "valid", mode: parsed };
230
+ return;
231
+ }
232
+ const rawText = String(raw);
233
+ const message = formatInvalidCursorMode(rawText);
234
+ cliCursorModeState = { kind: "invalid", raw: rawText, message };
235
+ if (hasUI) {
236
+ notify(message, "error");
237
+ return;
238
+ }
239
+ throw new Error(message);
240
+ }
241
+
242
+ function formatEffectiveCursorSettingSourcesLabel(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): string {
243
+ const effective = getEffectiveCursorSettingSources(raw);
244
+ const effectiveLabel = effective === undefined ? "none" : effective.join(",");
245
+ const rawLabel = raw?.trim() ? raw.trim() : "(unset → all)";
246
+ return `${rawLabel} (effective: ${effectiveLabel})`;
247
+ }
248
+
249
+ export function formatCursorToolsDebugReport(
250
+ pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
251
+ env: Record<string, string | undefined> = process.env,
252
+ ): string {
253
+ const bridgeEnabled = resolveCursorPiToolBridgeEnabled(env);
254
+ const manifestEnabled = resolveCursorToolManifestEnabled(env);
255
+ const lines = [
256
+ "Cursor tool surfaces (current session):",
257
+ `${CURSOR_PI_TOOL_BRIDGE_ENV}: ${bridgeEnabled ? "enabled" : "disabled"}`,
258
+ `${CURSOR_TOOL_MANIFEST_ENV}: ${manifestEnabled ? "enabled" : "disabled"}`,
259
+ `${CURSOR_SETTING_SOURCES_ENV}: ${formatEffectiveCursorSettingSourcesLabel(env[CURSOR_SETTING_SOURCES_ENV])}`,
260
+ ];
261
+
262
+ let bridgeSnapshot;
263
+ if (bridgeEnabled) {
264
+ try {
265
+ bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi);
266
+ } catch {
267
+ lines.push("Pi bridge snapshot: unavailable (extension tool APIs required).");
268
+ }
269
+ }
270
+
271
+ lines.push(buildCursorToolManifestText({ bridgeSnapshot, piBridgeEnabled: bridgeEnabled }));
272
+ return lines.join("\n");
273
+ }
274
+
275
+ function emitCursorToolsDebugReport(
276
+ pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
277
+ ctx: Pick<ExtensionContext, "hasUI" | "ui">,
278
+ ): void {
279
+ const report = formatCursorToolsDebugReport(pi);
280
+ if (ctx.hasUI) {
281
+ ctx.ui.notify(report, "info");
282
+ return;
283
+ }
284
+ console.log(report);
285
+ }
286
+
149
287
  export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
150
288
  const metadata = getCursorModelMetadata(modelId);
151
289
  if (!metadata) return undefined;
152
290
  return getEffectiveFast(metadata.baseModelId, modelId);
153
291
  }
154
292
 
155
- export function registerCursorFastControls(pi: CursorFastControlsExtensionApi): void {
293
+ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
156
294
  pi.registerFlag("cursor-fast", {
157
295
  description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
158
296
  type: "boolean",
@@ -165,6 +303,12 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
165
303
  default: false,
166
304
  });
167
305
 
306
+ pi.registerFlag("cursor-mode", {
307
+ description: "Set Cursor SDK conversation mode for this run: agent or plan",
308
+ type: "string",
309
+ default: "",
310
+ });
311
+
168
312
  pi.registerCommand("cursor-fast", {
169
313
  description: "Toggle Cursor fast mode for the selected Cursor model",
170
314
  handler: async (_args, ctx) => {
@@ -197,11 +341,60 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
197
341
  },
198
342
  });
199
343
 
344
+ pi.registerCommand("cursor-tools", {
345
+ description: "Show live Cursor tool surfaces for this session (maintainer debug)",
346
+ handler: async (_args, ctx) => {
347
+ emitCursorToolsDebugReport(pi, ctx);
348
+ },
349
+ });
350
+
351
+ pi.registerCommand("cursor-mode", {
352
+ description: "Set Cursor SDK conversation mode: agent or plan",
353
+ handler: async (args, ctx) => {
354
+ const usage = "Usage: /cursor-mode agent|plan";
355
+ const mode = parseCursorAgentMode(args);
356
+ if (!args.trim()) {
357
+ try {
358
+ ctx.ui.notify(`Cursor mode is ${getEffectiveCursorAgentMode()}. ${usage}`, "info");
359
+ } catch (error) {
360
+ ctx.ui.notify(`${error instanceof Error ? error.message : String(error)} ${usage}`, "error");
361
+ }
362
+ return;
363
+ }
364
+ if (!mode) {
365
+ ctx.ui.notify(`Invalid Cursor mode "${args.trim()}". ${usage}`, "error");
366
+ return;
367
+ }
368
+ if (cliCursorModeState.kind === "valid") {
369
+ ctx.ui.notify(`Cursor mode is forced to ${cliCursorModeState.mode} by --cursor-mode`, "info");
370
+ return;
371
+ }
372
+ const clearedInvalidCliMode = cliCursorModeState.kind === "invalid";
373
+ try {
374
+ persistCursorModePreference(pi, mode);
375
+ if (clearedInvalidCliMode) cliCursorModeState = { kind: "unset" };
376
+ } catch (error) {
377
+ updateCursorStatus(ctx);
378
+ ctx.ui.notify(`Failed to save Cursor mode preference: ${error instanceof Error ? error.message : String(error)}`, "error");
379
+ return;
380
+ }
381
+ updateCursorStatus(ctx);
382
+ ctx.ui.notify(
383
+ clearedInvalidCliMode
384
+ ? `Cursor mode set to ${mode}; cleared invalid --cursor-mode override`
385
+ : `Cursor mode set to ${mode}`,
386
+ "info",
387
+ );
388
+ },
389
+ });
390
+
200
391
  pi.on("session_start", async (_event, ctx) => {
201
392
  globalFastPreferences = loadGlobalFastPreferences();
202
393
  cliForceFast = pi.getFlag("cursor-fast") === true;
203
394
  cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
204
395
  restoreSessionFastPreferences(ctx);
396
+ restoreSessionCursorMode(ctx);
397
+ restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.hasUI, ctx.ui.notify.bind(ctx.ui));
205
398
  updateCursorStatus(ctx);
206
399
  });
207
400
 
@@ -214,9 +407,20 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
214
407
  });
215
408
  }
216
409
 
410
+ function resetCursorModeStateForTests(): void {
411
+ sessionCursorAgentMode = undefined;
412
+ cliCursorModeState = { kind: "unset" };
413
+ }
414
+
217
415
  export const __testUtils = {
218
416
  FAST_ENTRY_TYPE,
417
+ MODE_ENTRY_TYPE,
418
+ DEFAULT_CURSOR_AGENT_MODE,
219
419
  getConfigPath,
220
420
  loadGlobalFastPreferences,
221
421
  sessionFastPreferences,
422
+ getSessionCursorAgentMode: () => sessionCursorAgentMode,
423
+ getCliCursorAgentMode: () => (cliCursorModeState.kind === "valid" ? cliCursorModeState.mode : undefined),
424
+ getCliCursorModeState: () => cliCursorModeState,
425
+ resetCursorModeStateForTests,
222
426
  };
@@ -1,5 +1,6 @@
1
1
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
2
2
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
+ import { getCursorToolLifecycleLabelKind } from "./cursor-tool-presentation-registry.js";
3
4
  import { extractWebSearchQuery } from "./cursor-web-tool-activity.js";
4
5
  import { firstNonEmptyLine, getArray, getString, truncateArg } from "./cursor-transcript-utils.js";
5
6
  import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
@@ -39,7 +40,7 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
39
40
  const visibility = classifyCursorToolVisibility(toolCall);
40
41
  const args = visibility.args;
41
42
 
42
- switch (visibility.normalizedKey) {
43
+ switch (getCursorToolLifecycleLabelKind(visibility.normalizedKey)) {
43
44
  case "task": {
44
45
  return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
45
46
  }
@@ -49,26 +50,26 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
49
50
  case "mcp": {
50
51
  return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
51
52
  }
52
- case "generateimage": {
53
+ case "generateImage": {
53
54
  return scrubLifecycleDetail(getString(args, "prompt") ?? getString(args, "description"), apiKey) ?? "image generation";
54
55
  }
55
- case "recordscreen": {
56
+ case "recordScreen": {
56
57
  return scrubLifecycleDetail(getString(args, "mode"), apiKey) ?? "screen recording";
57
58
  }
58
- case "semsearch": {
59
+ case "semSearch": {
59
60
  return scrubLifecycleDetail(getString(args, "query"), apiKey) ?? "semantic search";
60
61
  }
61
- case "websearch": {
62
+ case "webSearch": {
62
63
  return scrubLifecycleDetail(extractWebSearchQuery(args), apiKey) ?? "web search";
63
64
  }
64
- case "webfetch": {
65
+ case "webFetch": {
65
66
  return "web fetch";
66
67
  }
67
- case "createplan": {
68
+ case "createPlan": {
68
69
  const plan = getString(args, "plan");
69
70
  return scrubLifecycleDetail(plan ? firstNonEmptyLine(plan) ?? plan : undefined, apiKey) ?? "plan";
70
71
  }
71
- case "updatetodos": {
72
+ case "updateTodos": {
72
73
  const todos = getArray(args, "todos") ?? getArray(args, "items");
73
74
  if (todos && todos.length > 0) return truncateArg(`${todos.length} item${todos.length === 1 ? "" : "s"}`);
74
75
  return "todos";
@@ -0,0 +1,41 @@
1
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+ import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.js";
3
+
4
+ export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
5
+
6
+ /**
7
+ * Representative @cursor/sdk@1.0.14 local-agent ToolType values; actual exposure can vary by run.
8
+ * See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
9
+ */
10
+ export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
11
+ "read, shell, grep, glob, ls, edit, write, delete, readLints, updateTodos, createPlan, task, generateImage, mcp, semSearch, recordScreen, and web search/fetch when exposed";
12
+
13
+ export function resolveCursorToolManifestEnabled(
14
+ env: Record<string, string | undefined> = process.env,
15
+ ): boolean {
16
+ return parseEnvBoolean(env[CURSOR_TOOL_MANIFEST_ENV], true);
17
+ }
18
+
19
+ export function buildCursorToolManifestText(options: {
20
+ bridgeSnapshot?: CursorPiToolBridgeSnapshot;
21
+ /** When false, bridge is off via PI_CURSOR_PI_TOOL_BRIDGE=0 (not merely empty). */
22
+ piBridgeEnabled?: boolean;
23
+ } = {}): string {
24
+ const piBridgeEnabled = options.piBridgeEnabled ?? true;
25
+ const lines = [
26
+ "Callable tool surfaces this run:",
27
+ `- Cursor SDK host tools (callable; not listed in MCP listTools): ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}.`,
28
+ "- Configured Cursor MCP servers: discovered at runtime via MCP listTools (depends on Cursor settings and PI_CURSOR_SETTING_SOURCES).",
29
+ ];
30
+ const bridgeTools = options.bridgeSnapshot?.tools ?? [];
31
+ if (!piBridgeEnabled) {
32
+ lines.push("- Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
33
+ } else if (bridgeTools.length === 0) {
34
+ lines.push("- Pi bridge: no pi__* tools exposed this run.");
35
+ } else {
36
+ const names = [...bridgeTools.map((tool) => tool.mcpToolName)].sort().join(", ");
37
+ lines.push(`- Pi bridge (call pi__* MCP names; pi shows real pi tool names): ${names}.`);
38
+ }
39
+ lines.push("- Not callable: cursor-replay-* IDs, pi history tool names, and transcript labels.");
40
+ return lines.join("\n");
41
+ }
@@ -1,106 +1,18 @@
1
- export const CURSOR_REPLAY_ACTIVITY_TOOL_NAME = "cursor";
2
-
3
- export const CURSOR_REPLAY_LEGACY_TOOL_NAMES = [
4
- "cursor_edit",
5
- "cursor_write",
6
- "cursor_read_lints",
7
- "cursor_delete",
8
- "cursor_update_todos",
9
- "cursor_task",
10
- "cursor_create_plan",
11
- "cursor_generate_image",
12
- "cursor_mcp",
13
- "cursor_sem_search",
14
- "cursor_record_screen",
15
- "cursor_web_search",
16
- "cursor_web_fetch",
17
- ] as const;
18
-
19
- export type CursorReplayLegacyToolName = (typeof CURSOR_REPLAY_LEGACY_TOOL_NAMES)[number];
20
- export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME | CursorReplayLegacyToolName;
21
-
22
- const CURSOR_REPLAY_SOURCE_TOOL_NAMES = {
23
- cursor_edit: "edit",
24
- cursor_write: "write",
25
- cursor_read_lints: "readLints",
26
- cursor_delete: "delete",
27
- cursor_update_todos: "updateTodos",
28
- cursor_task: "task",
29
- cursor_create_plan: "createPlan",
30
- cursor_generate_image: "generateImage",
31
- cursor_mcp: "MCP",
32
- cursor_sem_search: "semSearch",
33
- cursor_record_screen: "recordScreen",
34
- cursor_web_search: "web search",
35
- cursor_web_fetch: "web fetch",
36
- } as const satisfies Record<CursorReplayLegacyToolName, string>;
37
-
38
- const CURSOR_REPLAY_PROMPT_LABELS = {
39
- cursor_edit: "Cursor edit",
40
- cursor_write: "Cursor write",
41
- cursor_read_lints: "Cursor diagnostics",
42
- cursor_delete: "Cursor delete",
43
- cursor_update_todos: "Cursor todos",
44
- cursor_task: "Cursor task",
45
- cursor_create_plan: "Cursor plan",
46
- cursor_generate_image: "Cursor image generation",
47
- cursor_mcp: "Cursor MCP",
48
- cursor_sem_search: "Cursor semantic search",
49
- cursor_record_screen: "Cursor screen recording",
50
- cursor_web_search: "Cursor web search",
51
- cursor_web_fetch: "Cursor web fetch",
52
- } as const satisfies Record<CursorReplayLegacyToolName, string>;
53
-
54
- export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = {
55
- edit: "cursor_edit",
56
- write: "cursor_write",
57
- readLints: "cursor_read_lints",
58
- delete: "cursor_delete",
59
- updateTodos: "cursor_update_todos",
60
- task: "cursor_task",
61
- createPlan: "cursor_create_plan",
62
- generateImage: "cursor_generate_image",
63
- mcp: "cursor_mcp",
64
- semSearch: "cursor_sem_search",
65
- recordScreen: "cursor_record_screen",
66
- webSearch: "cursor_web_search",
67
- webFetch: "cursor_web_fetch",
68
- } as const satisfies Record<string, CursorReplayLegacyToolName>;
69
-
70
- export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
71
-
72
- export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
73
- return CURSOR_REPLAY_LEGACY_TOOL_NAMES.some((legacyToolName) => legacyToolName === toolName);
74
- }
75
-
76
- export function isCursorReplayToolName(toolName: string): toolName is CursorReplayToolName {
77
- return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
78
- }
79
-
80
- export function isExcludedFromCursorBridgeExposure(toolName: string): boolean {
81
- return isCursorReplayLegacyToolName(toolName) || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
82
- }
83
-
84
- export function getCursorReplaySourceToolName(toolName: CursorReplayLegacyToolName): string {
85
- return CURSOR_REPLAY_SOURCE_TOOL_NAMES[toolName];
86
- }
87
-
88
- export function getCursorReplayPromptLabel(toolName: string): string {
89
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
90
- if (isCursorReplayLegacyToolName(toolName)) return CURSOR_REPLAY_PROMPT_LABELS[toolName];
91
- return toolName;
92
- }
93
-
94
- export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): string {
95
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
96
- return CURSOR_REPLAY_PROMPT_LABELS[toolName];
97
- }
98
-
99
- export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
100
- return CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME[toolName as CursorReplayActivityToolName];
101
- }
102
-
103
- export function getCursorReplayActivityTitle(toolName: string): string | undefined {
104
- const labelKey = getCursorReplayActivityLabelKey(toolName);
105
- return labelKey ? getCursorReplayDisplayLabel(labelKey) : undefined;
106
- }
1
+ export {
2
+ CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME,
3
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
+ CURSOR_REPLAY_LEGACY_TOOL_NAMES,
5
+ getCursorReplayActivityLabelKey,
6
+ getCursorReplayActivityTitle,
7
+ getCursorReplayDisplayLabel,
8
+ getCursorReplayPromptLabel,
9
+ getCursorReplaySideEffectDescription,
10
+ getCursorReplayOperationLabel,
11
+ getCursorReplayWrapperLabel,
12
+ isCursorReplayLegacyToolName,
13
+ isCursorReplayToolName,
14
+ isExcludedFromCursorBridgeExposure,
15
+ type CursorReplayActivityToolName,
16
+ type CursorReplayLegacyToolName,
17
+ type CursorReplayToolName,
18
+ } from "./cursor-tool-presentation-registry.js";