pi-cursor-sdk 0.1.37 → 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 (73) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/docs/cursor-native-tool-replay.md +3 -3
  3. package/package.json +1 -1
  4. package/scripts/platform-smoke/card-detect.mjs +1 -1
  5. package/src/context-window-cache.ts +10 -14
  6. package/src/context.ts +1 -1
  7. package/src/cursor-agent-message-web-tools.ts +2 -1
  8. package/src/cursor-agents-context-registration.ts +18 -0
  9. package/src/cursor-agents-context.ts +21 -30
  10. package/src/cursor-edit-diff.ts +4 -2
  11. package/src/cursor-fallback-warning.ts +22 -0
  12. package/src/cursor-incomplete-tool-visibility.ts +5 -10
  13. package/src/cursor-live-run-coordinator.ts +1 -1
  14. package/src/cursor-mcp-timeout-override.ts +0 -2
  15. package/src/cursor-model-lifecycle.ts +72 -0
  16. package/src/cursor-native-replay-routing.ts +1 -1
  17. package/src/cursor-native-replay-trace.ts +1 -1
  18. package/src/cursor-native-tool-display-registration.ts +16 -28
  19. package/src/cursor-native-tool-display-replay.ts +4 -21
  20. package/src/cursor-native-tool-display-state.ts +1 -1
  21. package/src/cursor-native-tool-display-tools.ts +10 -17
  22. package/src/cursor-native-tool-names.ts +16 -0
  23. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  24. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  25. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  26. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  27. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  28. package/src/cursor-pi-tool-bridge.ts +7 -7
  29. package/src/cursor-provider-lazy.ts +51 -0
  30. package/src/cursor-provider-live-run-drain.ts +1 -1
  31. package/src/cursor-provider-run-finalizer.ts +5 -5
  32. package/src/cursor-provider-run-outcome.ts +0 -1
  33. package/src/cursor-provider-turn-coordinator.ts +4 -5
  34. package/src/cursor-provider-turn-display-router.ts +5 -1
  35. package/src/cursor-provider-turn-emit.ts +1 -1
  36. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  37. package/src/cursor-provider-turn-prepare.ts +13 -9
  38. package/src/cursor-provider-turn-runner.ts +3 -11
  39. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  40. package/src/cursor-provider-turn-send.ts +7 -2
  41. package/src/cursor-provider-turn-types.ts +1 -3
  42. package/src/cursor-provider.ts +3 -2
  43. package/src/cursor-question-tool.ts +5 -18
  44. package/src/cursor-record-utils.ts +42 -0
  45. package/src/cursor-replay-activity-builders.ts +16 -122
  46. package/src/cursor-replay-tool-details.ts +52 -80
  47. package/src/cursor-sdk-event-debug.ts +6 -6
  48. package/src/cursor-sensitive-text.ts +4 -4
  49. package/src/cursor-session-agent-lifecycle.ts +47 -0
  50. package/src/cursor-session-agent.ts +9 -47
  51. package/src/cursor-session-scope.ts +23 -4
  52. package/src/cursor-setting-sources.ts +8 -8
  53. package/src/cursor-skill-tool.ts +25 -32
  54. package/src/cursor-state.ts +66 -45
  55. package/src/cursor-tool-lifecycle.ts +16 -9
  56. package/src/cursor-tool-presentation-registry.ts +27 -18
  57. package/src/cursor-tool-result-display-readers.ts +185 -0
  58. package/src/cursor-tool-transcript.ts +17 -33
  59. package/src/cursor-tool-visibility.ts +9 -1
  60. package/src/cursor-transcript-tool-formatters.ts +23 -172
  61. package/src/cursor-transcript-tool-specs.ts +16 -41
  62. package/src/cursor-transcript-utils.ts +2 -34
  63. package/src/cursor-usage-accounting.ts +0 -6
  64. package/src/cursor-web-tool-activity.ts +4 -12
  65. package/src/cursor-web-tool-args.ts +1 -9
  66. package/src/index.ts +15 -16
  67. package/src/model-discovery.ts +5 -4
  68. package/src/model-list-cache.ts +37 -38
  69. package/src/cursor-native-tool-display.ts +0 -10
  70. package/src/cursor-provider-turn-api-key.ts +0 -1
  71. package/src/cursor-provider-turn-message-offset.ts +0 -15
  72. package/src/cursor-session-cwd.ts +0 -28
  73. package/src/cursor-tool-names.ts +0 -9
@@ -1,7 +1,7 @@
1
1
  import type { SendOptions } from "@cursor/sdk";
2
+ import { countCursorAgentMessages } from "./cursor-agent-message-web-tools.js";
2
3
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
3
4
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
4
- import { getCursorAgentMessageOffset } from "./cursor-provider-turn-message-offset.js";
5
5
  import type { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
6
6
  import type {
7
7
  CursorProviderTurnRunnerParams,
@@ -41,7 +41,12 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
41
41
  try {
42
42
  abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
43
43
  throwIfAborted();
44
- const cursorAgentMessageOffset = await getCursorAgentMessageOffset(agent.agentId, cwd, sdkEventDebug);
44
+ let cursorAgentMessageOffset: number | undefined;
45
+ try {
46
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
47
+ } catch (error) {
48
+ sdkEventDebug?.recordError("cursor_agent_message_count", error);
49
+ }
45
50
  throwIfAborted();
46
51
  sdkEventDebug?.recordSendMeta({
47
52
  mode: meta.sendPlan.mode,
@@ -61,7 +61,7 @@ export type CursorProviderTurnRuntime = DirectCursorProviderTurnRuntime | LiveCu
61
61
  * Send, finalize, and cleanup phases receive this immutable object instead of
62
62
  * keeping parallel liveRun/turnCoordinator/resource bags in sync by convention.
63
63
  */
64
- export interface PreparedCursorProviderTurn {
64
+ export interface CursorProviderTurnPrepareResult {
65
65
  agent: SDKAgent;
66
66
  cwd: string;
67
67
  payload: CursorProviderTurnSendPayload;
@@ -74,8 +74,6 @@ export interface PreparedCursorProviderTurn {
74
74
  runtime: CursorProviderTurnRuntime;
75
75
  }
76
76
 
77
- export type CursorProviderTurnPrepareResult = PreparedCursorProviderTurn;
78
-
79
77
  export interface CursorProviderTurnSend {
80
78
  run: Awaited<ReturnType<SDKAgent["send"]>>;
81
79
  cursorAgentMessageOffset: number | undefined;
@@ -8,6 +8,7 @@ import {
8
8
  type SimpleStreamOptions,
9
9
  } from "@earendil-works/pi-ai";
10
10
  import {
11
+ cursorLiveRuns,
11
12
  DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
12
13
  getPendingCursorLiveRun,
13
14
  hasTrailingUserMessagesAfterToolResults,
@@ -15,12 +16,12 @@ import {
15
16
  resetCursorNativeReplayIdleDisposeMs,
16
17
  setCursorNativeReplayIdleDisposeMs,
17
18
  } from "./cursor-provider-live-run-drain.js";
18
- import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
19
19
  import { disposeAllSessionCursorAgents } from "./cursor-session-agent.js";
20
20
  import { attachCursorSdkEventDebugPiStreamTap, type CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
21
21
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
22
22
  import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
23
- import { CursorProviderTurnRunner, resolveCursorApiKey } from "./cursor-provider-turn-runner.js";
23
+ import { resolveCursorApiKey } from "./cursor-api-key.js";
24
+ import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
24
25
 
25
26
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
26
27
  return {
@@ -1,8 +1,9 @@
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 { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { isCursorModel } from "./cursor-model.js";
5
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge.js";
5
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
6
+ import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
6
7
 
7
8
  export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
8
9
 
@@ -35,12 +36,7 @@ interface CursorQuestionDetails {
35
36
  cancelled: boolean;
36
37
  }
37
38
 
38
- interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
39
- on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
40
- on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
41
- on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
42
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
43
- }
39
+ interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools">, CursorModelLifecycleExtensionApi {}
44
40
 
45
41
  type RawQuestionOption = string | { label?: string; value?: string; description?: string };
46
42
 
@@ -232,16 +228,7 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
232
228
  },
233
229
  });
234
230
 
235
- pi.on("session_start", (_event, ctx) => {
236
- syncCursorQuestionToolForModel(pi, ctx.model);
237
- });
238
- pi.on("before_agent_start", (_event, ctx) => {
231
+ registerCursorModelLifecycle(pi, (ctx) => {
239
232
  syncCursorQuestionToolForModel(pi, ctx.model);
240
233
  });
241
- pi.on("turn_start", (_event, ctx) => {
242
- syncCursorQuestionToolForModel(pi, ctx.model);
243
- });
244
- pi.on("model_select", (event) => {
245
- syncCursorQuestionToolForModel(pi, event.model);
246
- });
247
234
  }
@@ -10,6 +10,48 @@ export function hasUsableText(value: string | undefined): value is string {
10
10
  return typeof value === "string" && value.trim().length > 0;
11
11
  }
12
12
 
13
+ export function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
14
+ const value = record?.[key];
15
+ return typeof value === "string" ? value : undefined;
16
+ }
17
+
18
+ export function getNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
19
+ const value = record?.[key];
20
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
21
+ }
22
+
23
+ export function getBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
24
+ const value = record?.[key];
25
+ return typeof value === "boolean" ? value : undefined;
26
+ }
27
+
28
+ export function getRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
29
+ return asRecord(record?.[key]);
30
+ }
31
+
32
+ export function getArray(record: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
33
+ const value = record?.[key];
34
+ return Array.isArray(value) ? value : undefined;
35
+ }
36
+
37
+ export 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 stringifyUnknown(value: unknown, options: { pretty?: boolean } = {}): string {
46
+ if (value === undefined) return "";
47
+ if (typeof value === "string") return value;
48
+ try {
49
+ return JSON.stringify(value, null, options.pretty ? 2 : undefined) ?? String(value);
50
+ } catch {
51
+ return String(value);
52
+ }
53
+ }
54
+
13
55
  export function getFirstStringByKeys(
14
56
  record: Record<string, unknown> | undefined,
15
57
  keys: readonly string[],
@@ -1,4 +1,3 @@
1
- import { scrubSensitiveText } from "./cursor-sensitive-text.js";
2
1
  import type {
3
2
  CursorReplayGenerateImageSummaryArgs,
4
3
  CursorReplayMcpSummaryArgs,
@@ -13,16 +12,20 @@ import type {
13
12
  CursorReplayWebSearchSummaryArgs,
14
13
  } from "./cursor-replay-summary-args.js";
15
14
  import type { CursorReplayGenerateImageDetailFields } from "./cursor-replay-tool-details.js";
16
- import { asRecord } from "./cursor-record-utils.js";
15
+ import { asRecord, getArray, getNumber, getString } from "./cursor-record-utils.js";
16
+ import { firstNonEmptyLine, formatDisplayPath, truncateArg } from "./cursor-transcript-utils.js";
17
17
  import {
18
- firstNonEmptyLine,
19
- formatDisplayPath,
20
- getArray,
21
- getNumber,
22
- getRecord,
23
- getString,
24
- truncateArg,
25
- } from "./cursor-transcript-utils.js";
18
+ collectTaskText,
19
+ getGenerateImageDisplayPath,
20
+ getGenerateImagePath,
21
+ readMcpDisplayResult,
22
+ getReadLintDiagnostics,
23
+ getReadLintPaths,
24
+ getTaskDescription,
25
+ getTodoItems,
26
+ getTodoTotalCount,
27
+ inferImageMimeType,
28
+ } from "./cursor-tool-result-display-readers.js";
26
29
  import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
27
30
 
28
31
  export interface CursorReplayActivityBuildContext {
@@ -48,14 +51,6 @@ export function buildDeleteReplayDetailFields({ args, result, options }: CursorR
48
51
  };
49
52
  }
50
53
 
51
- export function buildEmptyReplayDetailFields(): Record<string, never> {
52
- return {};
53
- }
54
-
55
- export function buildCollapsedReplayDetailFields(): { collapseDetailsByDefault: true } {
56
- return { collapseDetailsByDefault: true };
57
- }
58
-
59
54
  export function buildReadLintsReplaySummaryArgs({
60
55
  args,
61
56
  result,
@@ -75,7 +70,7 @@ export function buildTodoReplaySummaryArgs(
75
70
  result: CursorReplayActivityBuildContext["result"],
76
71
  ): CursorReplayTodoSummaryArgs {
77
72
  const todos = getTodoItems(args, result);
78
- const totalCount = getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
73
+ const totalCount = getTodoTotalCount(args, result, todos);
79
74
  const completedCount = todos.filter((todo) => todo.status === "completed").length;
80
75
  const inProgressCount = todos.filter((todo) => todo.status === "inProgress").length;
81
76
  const pendingCount = todos.filter((todo) => todo.status === "pending").length;
@@ -94,7 +89,7 @@ export function buildCreatePlanReplaySummaryArgs({ args, result }: CursorReplayA
94
89
  }
95
90
 
96
91
  export function buildTaskReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayTaskSummaryArgs {
97
- const description = getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
92
+ const description = getTaskDescription(args, result);
98
93
  const preview = firstNonEmptyLine(collectTaskText(result));
99
94
  return {
100
95
  description: truncateArg(description),
@@ -117,7 +112,7 @@ export function buildGenerateImageReplaySummaryArgs({
117
112
 
118
113
  export function buildMcpReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayMcpSummaryArgs {
119
114
  const toolName = getString(args, "toolName") ?? "mcp";
120
- const preview = getMcpResultPreview(result);
115
+ const preview = readMcpDisplayResult(result).preview;
121
116
  return {
122
117
  toolName: truncateArg(toolName),
123
118
  ...(preview ? { preview } : {}),
@@ -173,104 +168,3 @@ export function buildGenerateImageReplayDetailFields(
173
168
  expandedText: contentText,
174
169
  };
175
170
  }
176
-
177
- function getReadLintPaths(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string[] {
178
- const explicitPaths = Array.isArray(args.paths)
179
- ? args.paths.filter((entry): entry is string => typeof entry === "string")
180
- : typeof args.path === "string"
181
- ? [args.path]
182
- : [];
183
- const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
184
- .map((file) => getString(asRecord(file), "path"))
185
- .filter((entry): entry is string => Boolean(entry));
186
- return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
187
- }
188
-
189
- function getReadLintDiagnostics(result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string[] {
190
- const value = asRecord(result.value);
191
- const files = getArray(value, "fileDiagnostics") ?? [];
192
- const lines: string[] = [];
193
- for (const file of files) {
194
- const fileRecord = asRecord(file);
195
- const pathValue = getString(fileRecord, "path");
196
- const path = pathValue ? formatDisplayPath(pathValue, options.cwd) : "unknown";
197
- const diagnostics = getArray(fileRecord, "diagnostics") ?? [];
198
- for (const diagnostic of diagnostics) {
199
- const diagnosticRecord = asRecord(diagnostic);
200
- const severity = getString(diagnosticRecord, "severity") ?? "diagnostic";
201
- const message = getString(diagnosticRecord, "message") ?? "";
202
- const source = getString(diagnosticRecord, "source");
203
- lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
204
- }
205
- }
206
- return lines;
207
- }
208
-
209
- function getTodoItems(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"]): Array<{ content: string; status?: string }> {
210
- const value = asRecord(result.value);
211
- const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
212
- const todos: Array<{ content: string; status?: string }> = [];
213
- for (const todo of rawTodos) {
214
- const record = asRecord(todo);
215
- const content = getString(record, "content");
216
- if (!content) continue;
217
- const status = getString(record, "status");
218
- todos.push(status ? { content, status } : { content });
219
- }
220
- return todos;
221
- }
222
-
223
- function collectTaskText(result: CursorReplayActivityBuildContext["result"]): string {
224
- const value = asRecord(result.value);
225
- const success = getRecord(getRecord(value, "result"), "success");
226
- const command = getString(success, "command");
227
- const stdout = getString(success, "stdout");
228
- const interleavedOutput = getString(success, "interleavedOutput");
229
- const assistantMessages = (getArray(value, "conversationSteps") ?? [])
230
- .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
231
- .filter((entry): entry is string => Boolean(entry));
232
- const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
233
- return parts.join("\n");
234
- }
235
-
236
- function getGenerateImagePath(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"]): string | undefined {
237
- const value = asRecord(result.value);
238
- return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
239
- }
240
-
241
- function getGenerateImageDisplayPath(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string | undefined {
242
- const path = getGenerateImagePath(args, result);
243
- return path ? formatDisplayPath(path, options.cwd) : undefined;
244
- }
245
-
246
- function inferImageMimeType(path: string | undefined): string | undefined {
247
- const lower = path?.toLowerCase();
248
- if (!lower) return undefined;
249
- if (lower.endsWith(".png")) return "image/png";
250
- if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
251
- if (lower.endsWith(".gif")) return "image/gif";
252
- if (lower.endsWith(".webp")) return "image/webp";
253
- return undefined;
254
- }
255
-
256
- function getMcpContentText(entry: unknown): string | undefined {
257
- const record = asRecord(entry);
258
- const directText = getString(record, "text");
259
- if (directText) return directText;
260
- const nestedText = getRecord(record, "text");
261
- return getString(nestedText, "text");
262
- }
263
-
264
- function getMcpResultPreview(result: CursorReplayActivityBuildContext["result"]): string | undefined {
265
- if (result.status === "error") return undefined;
266
- const value = asRecord(result.value);
267
- const content = getArray(value, "content") ?? [];
268
- for (const entry of content) {
269
- const text = getMcpContentText(entry);
270
- if (text) {
271
- const line = firstNonEmptyLine(text);
272
- if (line) return truncateArg(scrubSensitiveText(line), 120);
273
- }
274
- }
275
- return undefined;
276
- }
@@ -1,3 +1,4 @@
1
+ import { asRecord, getBoolean, getNumber, getString } from "./cursor-record-utils.js";
1
2
  import { isCursorReplayActivitySourceName, type CursorReplayActivitySourceName } from "./cursor-replay-source-names.js";
2
3
 
3
4
  /** Replay detail variants keyed by replay card disposition, not SDK source tool alone. */
@@ -111,70 +112,51 @@ export type CursorReplayGenerateImageDetailFields = Pick<
111
112
  "summary" | "expandedText" | "imagePath" | "imageDisplayPath" | "imageMimeType"
112
113
  >;
113
114
 
114
- function isRecord(value: unknown): value is Record<string, unknown> {
115
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
116
- }
117
-
118
- function readOptionalString(record: Record<string, unknown>, key: string): string | undefined {
119
- const value = record[key];
120
- return typeof value === "string" ? value : undefined;
121
- }
122
-
123
- function readOptionalNumber(record: Record<string, unknown>, key: string): number | undefined {
124
- const value = record[key];
125
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
126
- }
127
-
128
- function readOptionalBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
129
- const value = record[key];
130
- return typeof value === "boolean" ? value : undefined;
131
- }
132
-
133
115
  function readSourceToolName(record: Record<string, unknown>): string | undefined {
134
- const sourceToolName = readOptionalString(record, "sourceToolName");
116
+ const sourceToolName = getString(record, "sourceToolName");
135
117
  return sourceToolName?.trim() ? sourceToolName.trim() : undefined;
136
118
  }
137
119
 
138
120
  function readVariant(record: Record<string, unknown>): string | undefined {
139
- const variant = readOptionalString(record, "variant");
121
+ const variant = getString(record, "variant");
140
122
  return variant?.trim() ? variant.trim() : undefined;
141
123
  }
142
124
 
143
125
  function parseCursorReplayNativeEditDetails(record: Record<string, unknown>): CursorReplayNativeEditDetails {
144
126
  return {
145
127
  variant: "nativeEdit",
146
- path: readOptionalString(record, "path"),
147
- linesAdded: readOptionalNumber(record, "linesAdded"),
148
- linesRemoved: readOptionalNumber(record, "linesRemoved"),
149
- diffString: readOptionalString(record, "diffString"),
150
- diff: readOptionalString(record, "diff"),
151
- firstChangedLine: readOptionalNumber(record, "firstChangedLine"),
152
- summary: readOptionalString(record, "summary"),
153
- expandedText: readOptionalString(record, "expandedText"),
128
+ path: getString(record, "path"),
129
+ linesAdded: getNumber(record, "linesAdded"),
130
+ linesRemoved: getNumber(record, "linesRemoved"),
131
+ diffString: getString(record, "diffString"),
132
+ diff: getString(record, "diff"),
133
+ firstChangedLine: getNumber(record, "firstChangedLine"),
134
+ summary: getString(record, "summary"),
135
+ expandedText: getString(record, "expandedText"),
154
136
  };
155
137
  }
156
138
 
157
139
  function parseCursorReplayNativeWriteDetails(record: Record<string, unknown>): CursorReplayNativeWriteDetails {
158
140
  return {
159
141
  variant: "nativeWrite",
160
- path: readOptionalString(record, "path"),
161
- linesCreated: readOptionalNumber(record, "linesCreated"),
162
- fileSize: readOptionalNumber(record, "fileSize"),
163
- fileContentAfterWrite: readOptionalString(record, "fileContentAfterWrite"),
164
- expandedText: readOptionalString(record, "expandedText"),
165
- summary: readOptionalString(record, "summary"),
142
+ path: getString(record, "path"),
143
+ linesCreated: getNumber(record, "linesCreated"),
144
+ fileSize: getNumber(record, "fileSize"),
145
+ fileContentAfterWrite: getString(record, "fileContentAfterWrite"),
146
+ expandedText: getString(record, "expandedText"),
147
+ summary: getString(record, "summary"),
166
148
  };
167
149
  }
168
150
 
169
151
  function parseCursorReplayGenerateImageDetails(record: Record<string, unknown>): CursorReplayGenerateImageDetails {
170
- const collapseDetailsByDefault = readOptionalBoolean(record, "collapseDetailsByDefault");
152
+ const collapseDetailsByDefault = getBoolean(record, "collapseDetailsByDefault");
171
153
  return {
172
154
  variant: "generateImage",
173
- imagePath: readOptionalString(record, "imagePath"),
174
- imageDisplayPath: readOptionalString(record, "imageDisplayPath"),
175
- imageMimeType: readOptionalString(record, "imageMimeType"),
176
- summary: readOptionalString(record, "summary"),
177
- expandedText: readOptionalString(record, "expandedText"),
155
+ imagePath: getString(record, "imagePath"),
156
+ imageDisplayPath: getString(record, "imageDisplayPath"),
157
+ imageMimeType: getString(record, "imageMimeType"),
158
+ summary: getString(record, "summary"),
159
+ expandedText: getString(record, "expandedText"),
178
160
  ...(collapseDetailsByDefault !== undefined ? { collapseDetailsByDefault } : {}),
179
161
  };
180
162
  }
@@ -188,16 +170,16 @@ function parseCursorReplayActivityDetails(
188
170
  variant: "activity",
189
171
  sourceToolName,
190
172
  title,
191
- summary: readOptionalString(record, "summary"),
192
- expandedText: readOptionalString(record, "expandedText"),
193
- collapseDetailsByDefault: readOptionalBoolean(record, "collapseDetailsByDefault"),
194
- path: readOptionalString(record, "path"),
195
- fileSize: readOptionalNumber(record, "fileSize"),
196
- diffString: readOptionalString(record, "diffString"),
197
- diff: readOptionalString(record, "diff"),
198
- linesAdded: readOptionalNumber(record, "linesAdded"),
199
- linesRemoved: readOptionalNumber(record, "linesRemoved"),
200
- fileContentAfterWrite: readOptionalString(record, "fileContentAfterWrite"),
173
+ summary: getString(record, "summary"),
174
+ expandedText: getString(record, "expandedText"),
175
+ collapseDetailsByDefault: getBoolean(record, "collapseDetailsByDefault"),
176
+ path: getString(record, "path"),
177
+ fileSize: getNumber(record, "fileSize"),
178
+ diffString: getString(record, "diffString"),
179
+ diff: getString(record, "diff"),
180
+ linesAdded: getNumber(record, "linesAdded"),
181
+ linesRemoved: getNumber(record, "linesRemoved"),
182
+ fileContentAfterWrite: getString(record, "fileContentAfterWrite"),
201
183
  };
202
184
  }
203
185
 
@@ -212,8 +194,8 @@ function parseCursorReplayGenericFallbackDetails(
212
194
  return {
213
195
  variant: "genericFallback",
214
196
  sourceToolName: brandCursorReplayUnknownSourceToolName(sourceToolName),
215
- summary: readOptionalString(record, "summary"),
216
- expandedText: readOptionalString(record, "expandedText"),
197
+ summary: getString(record, "summary"),
198
+ expandedText: getString(record, "expandedText"),
217
199
  };
218
200
  }
219
201
 
@@ -237,10 +219,8 @@ export function resolveIncompleteReplayActivitySourceToolName(
237
219
  return resolveParseActivitySourceToolName(sourceToolName);
238
220
  }
239
221
 
240
- type CursorReplayVariantParser = (record: Record<string, unknown>) => CursorReplayToolDetails | undefined;
241
-
242
222
  function parseActivityVariantDetails(record: Record<string, unknown>): CursorReplayActivityDetails | undefined {
243
- const title = readOptionalString(record, "title")?.trim();
223
+ const title = getString(record, "title")?.trim();
244
224
  if (!title) return undefined;
245
225
  return parseCursorReplayActivityDetails(
246
226
  record,
@@ -249,31 +229,23 @@ function parseActivityVariantDetails(record: Record<string, unknown>): CursorRep
249
229
  );
250
230
  }
251
231
 
252
- const CURRENT_REPLAY_VARIANT_PARSERS: Readonly<Record<CursorReplayToolDetailsVariant, CursorReplayVariantParser>> = {
253
- nativeEdit: parseCursorReplayNativeEditDetails,
254
- nativeWrite: parseCursorReplayNativeWriteDetails,
255
- generateImage: parseCursorReplayGenerateImageDetails,
256
- activity: parseActivityVariantDetails,
257
- genericFallback: (record) => parseCursorReplayGenericFallbackDetails(record, readSourceToolName(record) ?? "tool"),
258
- };
259
-
260
232
  export function parseCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
261
- if (!isRecord(value)) return undefined;
262
- const variant = readVariant(value);
263
- if (!variant) return undefined;
264
- return CURRENT_REPLAY_VARIANT_PARSERS[variant as CursorReplayToolDetailsVariant]?.(value);
265
- }
266
-
267
- export function buildCursorReplayNativeEditDetails(
268
- fields: Omit<CursorReplayNativeEditDetails, "variant">,
269
- ): CursorReplayNativeEditDetails {
270
- return { variant: "nativeEdit", ...fields };
271
- }
272
-
273
- export function buildCursorReplayNativeWriteDetails(
274
- fields: Omit<CursorReplayNativeWriteDetails, "variant">,
275
- ): CursorReplayNativeWriteDetails {
276
- return { variant: "nativeWrite", ...fields };
233
+ const record = asRecord(value);
234
+ if (!record) return undefined;
235
+ switch (readVariant(record)) {
236
+ case "nativeEdit":
237
+ return parseCursorReplayNativeEditDetails(record);
238
+ case "nativeWrite":
239
+ return parseCursorReplayNativeWriteDetails(record);
240
+ case "generateImage":
241
+ return parseCursorReplayGenerateImageDetails(record);
242
+ case "activity":
243
+ return parseActivityVariantDetails(record);
244
+ case "genericFallback":
245
+ return parseCursorReplayGenericFallbackDetails(record, readSourceToolName(record) ?? "tool");
246
+ default:
247
+ return undefined;
248
+ }
277
249
  }
278
250
 
279
251
  export function assembleCursorReplayActivityDetails(
@@ -7,6 +7,7 @@ import type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-
7
7
  import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-diagnostics.js";
8
8
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge-types.js";
9
9
  import type { CursorLiveQueuedEvent } from "./cursor-live-run-coordinator.js";
10
+ import { asRecord } from "./cursor-record-utils.js";
10
11
  import { getCursorSessionFile } from "./cursor-session-scope.js";
11
12
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
12
13
  import {
@@ -93,11 +94,10 @@ interface CursorSdkRunLike {
93
94
  }
94
95
 
95
96
  function eventType(value: unknown): string {
96
- if (value && typeof value === "object") {
97
- if ("type" in value && typeof value.type === "string") return value.type;
98
- if ("event" in value && typeof value.event === "string") return value.event;
99
- if ("kind" in value && typeof value.kind === "string") return value.kind;
100
- }
97
+ const record = asRecord(value);
98
+ if (typeof record?.type === "string") return record.type;
99
+ if (typeof record?.event === "string") return record.event;
100
+ if (typeof record?.kind === "string") return record.kind;
101
101
  return "unknown";
102
102
  }
103
103
 
@@ -106,7 +106,7 @@ function resolveCursorSdkEventDebugStderrEnabled(env: Record<string, string | un
106
106
  }
107
107
 
108
108
  function isNodeErrorWithCode(error: unknown, code: string): boolean {
109
- return typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === code;
109
+ return asRecord(error)?.code === code;
110
110
  }
111
111
 
112
112
  function snapshotCursorSdkEventDebugRecord(record: unknown): unknown {
@@ -1,3 +1,4 @@
1
+ import { asRecord } from "./cursor-record-utils.js";
1
2
  import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
3
  /** Provider-facing wrapper; canonical scrubbing lives in shared/cursor-sensitive-text.mjs. */
3
4
  import { scrubSensitiveText as scrubSensitiveTextJs } from "../shared/cursor-sensitive-text.mjs";
@@ -9,10 +10,9 @@ export function scrubSensitiveText(text: string, apiKey?: string): string {
9
10
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
10
11
  if (typeof value === "string") return scrubSensitiveText(value, apiKey);
11
12
  if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
12
- if (value && typeof value === "object") {
13
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
14
- }
15
- return value;
13
+ const record = asRecord(value);
14
+ if (!record) return value;
15
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
16
16
  }
17
17
 
18
18
  export function scrubPiToolDisplay(display: CursorPiToolDisplay, apiKey?: string): CursorPiToolDisplay {
@@ -0,0 +1,47 @@
1
+ import type {
2
+ ExtensionHandler,
3
+ SessionBeforeTreeEvent,
4
+ SessionCompactEvent,
5
+ SessionShutdownEvent,
6
+ SessionTreeEvent,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
9
+
10
+ export interface CursorSessionAgentLifecycleExtensionApi {
11
+ on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
12
+ on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
13
+ on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent>): void;
14
+ on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
15
+ on(event: "model_select", handler: () => Promise<void> | void): void;
16
+ }
17
+
18
+ export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
19
+ onCursorSessionScopeKeyChange(async (previousScopeKey) => {
20
+ const { disposeSessionCursorAgent } = await import("./cursor-session-agent.js");
21
+ await disposeSessionCursorAgent(previousScopeKey);
22
+ });
23
+ pi.on("session_shutdown", async (event) => {
24
+ const { disposeSessionCursorAgent, resetSessionCursorAgent } = await import("./cursor-session-agent.js");
25
+ if (event.reason === "reload") {
26
+ await resetSessionCursorAgent();
27
+ return;
28
+ }
29
+ await disposeSessionCursorAgent();
30
+ });
31
+ pi.on("session_compact", async () => {
32
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
33
+ invalidateSessionAgent();
34
+ });
35
+ pi.on("session_before_tree", async () => {
36
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
37
+ invalidateSessionAgent();
38
+ });
39
+ pi.on("session_tree", async () => {
40
+ const { resetSessionCursorAgent } = await import("./cursor-session-agent.js");
41
+ await resetSessionCursorAgent();
42
+ });
43
+ pi.on("model_select", async () => {
44
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
45
+ invalidateSessionAgent();
46
+ });
47
+ }