pi-cursor-sdk 0.1.15 → 0.1.17

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 (46) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +20 -8
  3. package/docs/cursor-live-smoke-checklist.md +267 -0
  4. package/docs/cursor-model-ux-spec.md +15 -5
  5. package/docs/cursor-native-tool-replay.md +16 -5
  6. package/package.json +12 -5
  7. package/scripts/steering-rpc-smoke.mjs +238 -0
  8. package/scripts/tmux-live-smoke.sh +418 -0
  9. package/scripts/validate-smoke-jsonl.mjs +152 -0
  10. package/src/context.ts +180 -5
  11. package/src/cursor-bridge-contract.ts +27 -0
  12. package/src/cursor-edit-diff.ts +11 -0
  13. package/src/cursor-env-boolean.ts +22 -0
  14. package/src/cursor-live-run-accounting.ts +65 -0
  15. package/src/cursor-live-run-coordinator.ts +483 -0
  16. package/src/cursor-native-tool-display-registration.ts +93 -0
  17. package/src/cursor-native-tool-display-replay.ts +465 -0
  18. package/src/cursor-native-tool-display-state.ts +78 -0
  19. package/src/cursor-native-tool-display-tools.ts +102 -0
  20. package/src/cursor-native-tool-display.ts +10 -639
  21. package/src/cursor-partial-content-emitter.ts +121 -0
  22. package/src/cursor-pi-tool-bridge-abort.ts +133 -0
  23. package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
  24. package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
  25. package/src/cursor-pi-tool-bridge-run.ts +384 -0
  26. package/src/cursor-pi-tool-bridge-server.ts +182 -0
  27. package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
  28. package/src/cursor-pi-tool-bridge-types.ts +80 -0
  29. package/src/cursor-pi-tool-bridge.ts +77 -602
  30. package/src/cursor-provider-live-run-drain.ts +379 -0
  31. package/src/cursor-provider-turn-coordinator.ts +456 -0
  32. package/src/cursor-provider.ts +133 -1092
  33. package/src/cursor-question-tool.ts +7 -2
  34. package/src/cursor-record-utils.ts +26 -0
  35. package/src/cursor-sdk-output-filter.ts +100 -0
  36. package/src/cursor-sensitive-text.ts +37 -0
  37. package/src/cursor-session-agent.ts +372 -0
  38. package/src/cursor-session-cwd.ts +14 -19
  39. package/src/cursor-session-scope.ts +65 -0
  40. package/src/cursor-state.ts +38 -10
  41. package/src/cursor-tool-transcript.ts +28 -1229
  42. package/src/cursor-transcript-tool-formatters.ts +641 -0
  43. package/src/cursor-transcript-tool-specs.ts +441 -0
  44. package/src/cursor-transcript-utils.ts +276 -0
  45. package/src/cursor-usage-accounting.ts +71 -0
  46. package/src/index.ts +20 -3
@@ -1,755 +1,21 @@
1
- import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
- import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayDisplayLabel } from "./cursor-tool-names.js";
4
-
5
- const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
6
- const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
7
- const DEFAULT_MAX_LIST_ITEMS = 200;
8
- const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
9
- const DEFAULT_READ_TRANSCRIPT_LINES = 12;
10
- const DEFAULT_NATIVE_READ_DISPLAY_LINES = 20;
11
- const LOCAL_READ_PREVIEW_NOTICE =
12
- "[local file preview at transcript time; Cursor read result content was unavailable]";
13
-
14
- interface TranscriptOptions {
15
- maxChars?: number;
16
- maxLines?: number;
17
- maxListItems?: number;
18
- cwd?: string;
19
- }
20
-
21
- interface PiToolDisplayResult {
22
- content: Array<{ type: "text"; text: string }>;
23
- details?: unknown;
24
- }
25
-
26
- export interface CursorPiToolDisplay {
27
- toolName: string;
28
- args: Record<string, unknown>;
29
- result: PiToolDisplayResult;
30
- isError: boolean;
31
- }
32
-
33
- interface NormalizedResult {
34
- status: string | undefined;
35
- value: unknown;
36
- error: unknown;
37
- }
38
-
39
- function asRecord(value: unknown): Record<string, unknown> | undefined {
40
- return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
41
- }
42
-
43
- function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
44
- const value = record?.[key];
45
- return typeof value === "string" ? value : undefined;
46
- }
47
-
48
- function getNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
49
- const value = record?.[key];
50
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
51
- }
52
-
53
- function getBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
54
- const value = record?.[key];
55
- return typeof value === "boolean" ? value : undefined;
56
- }
57
-
58
- function getRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
59
- return asRecord(record?.[key]);
60
- }
61
-
62
- function getArray(record: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
63
- const value = record?.[key];
64
- return Array.isArray(value) ? value : undefined;
65
- }
66
-
67
- function getToolName(toolCall: unknown): string {
68
- const record = asRecord(toolCall);
69
- return getString(record, "name") ?? getString(record, "type") ?? getString(record, "toolName") ?? "unknown";
70
- }
71
-
72
- function getToolArgs(toolCall: unknown): Record<string, unknown> {
73
- const record = asRecord(toolCall);
74
- return getRecord(record, "args") ?? getRecord(record, "input") ?? {};
75
- }
76
-
77
- function getToolResult(toolCall: unknown): unknown {
78
- const record = asRecord(toolCall);
79
- return record?.result;
80
- }
81
-
82
- function normalizeToolName(name: string): string {
83
- const normalized = name.replace(/\s+/g, " ").trim();
84
- const normalizedKey = normalized.toLowerCase();
85
- switch (normalizedKey) {
86
- case "read_file":
87
- return "read";
88
- case "list_dir":
89
- return "ls";
90
- case "run_terminal_cmd":
91
- case "terminal":
92
- case "bash":
93
- case "shell":
94
- return "shell";
95
- case "grep_search":
96
- case "search":
97
- return "grep";
98
- case "file_search":
99
- return "glob";
100
- case "write_file":
101
- case "writefile":
102
- return "write";
103
- case "strreplace":
104
- case "str_replace":
105
- case "str-replace":
106
- case "edit_file":
107
- case "editfile":
108
- case "edit_notebook":
109
- case "editnotebook":
110
- case "notebook_edit":
111
- case "notebookedit":
112
- return "edit";
113
- default:
114
- return normalized || "unknown";
115
- }
116
- }
117
-
118
- function normalizeResult(result: unknown): NormalizedResult {
119
- const record = asRecord(result);
120
- const status = getString(record, "status");
121
- if (status === "success" || status === "error") {
122
- return { status, value: record?.value, error: record?.error };
123
- }
124
- return { status, value: result, error: undefined };
125
- }
126
-
127
- function stringifyUnknown(value: unknown): string {
128
- if (value === undefined) return "";
129
- if (typeof value === "string") return value;
130
- try {
131
- return JSON.stringify(value, null, 2) ?? String(value);
132
- } catch {
133
- return String(value);
134
- }
135
- }
136
-
137
- function limitText(text: string, options: TranscriptOptions = {}, knownTotalLines?: number): string {
138
- const maxChars = options.maxChars ?? DEFAULT_MAX_TRANSCRIPT_CHARS;
139
- const maxLines = options.maxLines ?? DEFAULT_MAX_TRANSCRIPT_LINES;
140
- const lines = text.split("\n");
141
- let limitedLines = lines.slice(0, maxLines);
142
- let limited = limitedLines.join("\n");
143
- let truncatedLines = Math.max((knownTotalLines ?? lines.length) - limitedLines.length, 0);
144
- let truncatedChars = 0;
145
-
146
- if (limited.length > maxChars) {
147
- truncatedChars += limited.length - maxChars;
148
- limited = limited.slice(0, maxChars);
149
- limitedLines = limited.split("\n");
150
- truncatedLines = Math.max(truncatedLines, Math.max((knownTotalLines ?? lines.length) - limitedLines.length, 0));
151
- }
152
- if (text.length > limited.length) {
153
- truncatedChars += Math.max(text.length - limited.length - truncatedChars, 0);
154
- }
155
-
156
- const suffixParts: string[] = [];
157
- if (truncatedLines > 0) suffixParts.push(`${truncatedLines} more lines`);
158
- if (truncatedChars > 0 && truncatedLines === 0) suffixParts.push(`${truncatedChars} more chars`);
159
- return suffixParts.length > 0 ? `${limited}\n... (${suffixParts.join(", ")} truncated)` : limited;
160
- }
161
-
162
- function limitItems<T>(items: T[], options: TranscriptOptions = {}): { items: T[]; omitted: number } {
163
- const maxListItems = options.maxListItems ?? DEFAULT_MAX_LIST_ITEMS;
164
- return { items: items.slice(0, maxListItems), omitted: Math.max(items.length - maxListItems, 0) };
165
- }
166
-
167
- function joinSections(header: string, body?: string): string {
168
- const trimmedBody = body?.trimEnd();
169
- return trimmedBody ? `${header}\n\n${trimmedBody}\n` : `${header}\n`;
170
- }
171
-
172
- function formatError(error: unknown): string {
173
- const text = stringifyUnknown(error).trim();
174
- return text ? `Error: ${text}` : "Error";
175
- }
176
-
177
- function formatDisplayPath(path: string, cwd = process.cwd()): string {
178
- const trimmed = path.trim();
179
- if (!trimmed) return trimmed;
180
- if (!isAbsolute(trimmed)) return trimmed;
181
- const relativePath = relative(cwd, trimmed);
182
- if (!relativePath || relativePath === "") return ".";
183
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) return trimmed;
184
- return relativePath;
185
- }
186
-
187
- function formatDiffPath(path: string, cwd = process.cwd()): string {
188
- if (path === "/dev/null") return path;
189
- return formatDisplayPath(path, cwd);
190
- }
191
-
192
- function formatDiffHeaderLine(line: string, options: TranscriptOptions): string {
193
- const match = /^(---|\+\+\+)\s+((?:[ab]\/)?)(.+)$/.exec(line);
194
- if (!match) return line;
195
- const [, marker, prefix, rawPath] = match;
196
- if (!prefix && rawPath !== "/dev/null") return line;
197
- const displayPath = formatDiffPath(rawPath, options.cwd);
198
- return `${marker} ${prefix}${displayPath}`;
199
- }
200
-
201
- function formatDiffString(diff: string | undefined, options: TranscriptOptions): string | undefined {
202
- return diff
203
- ?.split("\n")
204
- .map((line) => formatDiffHeaderLine(line, options))
205
- .join("\n");
206
- }
207
-
208
- function resolveFilePath(path: string, cwd = process.cwd()): string {
209
- return isAbsolute(path) ? path : resolve(cwd, path);
210
- }
211
-
212
- function isPathWithinCwd(filePath: string, cwd = process.cwd()): boolean {
213
- const relativePath = relative(cwd, filePath);
214
- return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
215
- }
216
-
217
- function isSensitivePreviewPath(filePath: string): boolean {
218
- const segments = filePath.split(/[\\/]+/).map((segment) => segment.toLowerCase());
219
- const basename = segments.at(-1) ?? "";
220
- return (
221
- segments.includes(".ssh") ||
222
- segments.includes("secrets") ||
223
- basename === ".env" ||
224
- basename.startsWith(".env.") ||
225
- basename === ".npmrc" ||
226
- basename === ".netrc" ||
227
- basename === "credentials" ||
228
- basename === "id_rsa" ||
229
- basename === "id_ed25519" ||
230
- /\.(?:pem|key|p12|pfx)$/i.test(basename)
231
- );
232
- }
233
-
234
- function readFilePreview(path: string, options: TranscriptOptions): string | undefined {
235
- const cwd = options.cwd ?? process.cwd();
236
- const filePath = resolveFilePath(path, cwd);
237
-
238
- const maxChars = options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS;
239
- const maxBytes = Math.max(8192, maxChars * 4);
240
- let fd: number | undefined;
241
- try {
242
- const realCwd = realpathSync(cwd);
243
- const realFilePath = realpathSync(filePath);
244
- if (!isPathWithinCwd(realFilePath, realCwd) || isSensitivePreviewPath(filePath) || isSensitivePreviewPath(realFilePath)) return undefined;
245
-
246
- const stat = statSync(realFilePath);
247
- if (!stat.isFile()) return undefined;
248
- fd = openSync(realFilePath, "r");
249
- const buffer = Buffer.alloc(Math.min(stat.size, maxBytes));
250
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
251
- const text = buffer.toString("utf8", 0, bytesRead);
252
- if (text.includes("\0")) return undefined;
253
- return text;
254
- } catch {
255
- return undefined;
256
- } finally {
257
- if (fd !== undefined) closeSync(fd);
258
- }
259
- }
260
-
261
- function formatPathArg(args: Record<string, unknown>, options: TranscriptOptions, key = "path"): string | undefined {
262
- const path = args[key];
263
- return typeof path === "string" && path.trim() ? formatDisplayPath(path, options.cwd) : undefined;
264
- }
265
-
266
- function getReadContent(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
267
- const rawPath = typeof args.path === "string" ? args.path : undefined;
268
- const readOptions = {
269
- ...options,
270
- maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
271
- maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
272
- };
273
- const value = asRecord(result.value);
274
- const resultContent = getString(value, "content");
275
- if (resultContent && resultContent.length > 0) return resultContent;
276
- if (!rawPath) return stringifyUnknown(result.value);
277
- const localPreview = readFilePreview(rawPath, readOptions);
278
- return localPreview ? `${LOCAL_READ_PREVIEW_NOTICE}\n${localPreview}` : stringifyUnknown(result.value);
279
- }
280
-
281
- function formatRead(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
282
- const rawPath = typeof args.path === "string" ? args.path : undefined;
283
- const path = rawPath ? formatDisplayPath(rawPath, options.cwd) : "unknown";
284
- if (result.status === "error") return joinSections(`read ${path}`, formatError(result.error));
285
-
286
- const value = asRecord(result.value);
287
- const totalLines = getNumber(value, "totalLines");
288
- const readOptions = {
289
- ...options,
290
- maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
291
- maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
292
- };
293
- return joinSections(`read ${path}`, limitText(getReadContent(args, result, options), readOptions, totalLines));
294
- }
295
-
296
- function buildReadDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
297
- const rawPath = typeof args.path === "string" ? args.path : undefined;
298
- return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
299
- }
300
-
301
- function buildPathDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
302
- const rawPath = typeof args.path === "string" ? args.path : undefined;
303
- return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
304
- }
305
-
306
- function buildWriteDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
307
- const displayArgs = buildPathDisplayArgs(args, options);
308
- const content = getCursorWriteArgContent(args);
309
- return content === undefined ? displayArgs : { ...displayArgs, content };
310
- }
311
-
312
- type NativeEditReplacement = { oldText: string; newText: string };
313
- type NativeEditDisplayArgs = { path: string; edits: NativeEditReplacement[] };
314
-
315
- const CURSOR_EDIT_PATH_KEYS = ["path", "filePath", "file_path"] as const;
316
- const CURSOR_EDIT_OLD_TEXT_KEYS = ["oldText", "old_text", "oldString", "old_string", "oldStr", "old_str"] as const;
317
- const CURSOR_EDIT_NEW_TEXT_KEYS = ["newText", "new_text", "newString", "new_string", "newStr", "new_str"] as const;
318
- const CURSOR_NOTEBOOK_EDIT_ARG_KEYS = ["cellId", "cell_id", "cellIndex", "cell_index", "cellType", "cell_type", "notebookPath", "notebook_path"] as const;
319
-
320
- function getFirstStringByKeys(record: Record<string, unknown>, keys: readonly string[]): string | undefined {
321
- for (const key of keys) {
322
- const value = record[key];
323
- if (typeof value === "string") return value;
324
- }
325
- return undefined;
326
- }
327
-
328
- function getCursorEditPathArg(args: Record<string, unknown>): string | undefined {
329
- const path = getFirstStringByKeys(args, CURSOR_EDIT_PATH_KEYS);
330
- return path?.trim() ? path : undefined;
331
- }
332
-
333
- function isCursorNotebookEditToolName(toolName: string): boolean {
334
- const normalized = toolName.replace(/[\s_-]+/g, "").toLowerCase();
335
- return normalized === "editnotebook" || normalized === "notebookedit";
336
- }
337
-
338
- function isCursorStrReplaceToolName(toolName: string): boolean {
339
- const normalized = toolName.replace(/[\s_-]+/g, "").toLowerCase();
340
- return normalized === "strreplace";
341
- }
342
-
343
- function hasAnyKey(record: Record<string, unknown>, keys: readonly string[]): boolean {
344
- return keys.some((key) => record[key] !== undefined);
345
- }
346
-
347
- function isNotebookPath(path: string | undefined): boolean {
348
- return path?.toLowerCase().endsWith(".ipynb") === true;
349
- }
350
-
351
- function isCursorNotebookEditActivity(rawToolName: string, args: Record<string, unknown>): boolean {
352
- if (isCursorNotebookEditToolName(rawToolName)) return true;
353
- if (hasAnyKey(args, CURSOR_NOTEBOOK_EDIT_ARG_KEYS)) return true;
354
- return !isCursorStrReplaceToolName(rawToolName) && isNotebookPath(getCursorEditPathArg(args));
355
- }
356
-
357
- function asNativeEditReplacement(value: unknown): NativeEditReplacement | undefined {
358
- const record = asRecord(value);
359
- const oldText = record ? getFirstStringByKeys(record, CURSOR_EDIT_OLD_TEXT_KEYS) : undefined;
360
- const newText = record ? getFirstStringByKeys(record, CURSOR_EDIT_NEW_TEXT_KEYS) : undefined;
361
- if (typeof oldText !== "string" || oldText.length === 0 || typeof newText !== "string") return undefined;
362
- return { oldText, newText };
363
- }
364
-
365
- function getNativeEditReplacementsFromArgs(args: Record<string, unknown>): NativeEditReplacement[] | undefined {
366
- const edits = getArray(args, "edits")?.map(asNativeEditReplacement);
367
- if (edits && edits.length > 0 && edits.every((edit): edit is NativeEditReplacement => edit !== undefined)) return edits;
368
-
369
- const singleEdit = asNativeEditReplacement(args);
370
- return singleEdit ? [singleEdit] : undefined;
371
- }
372
-
373
- function buildNativeEditDisplayArgs(rawToolName: string, args: Record<string, unknown>, options: TranscriptOptions): NativeEditDisplayArgs | undefined {
374
- if (isCursorNotebookEditActivity(rawToolName, args)) return undefined;
375
- const rawPath = getCursorEditPathArg(args);
376
- const edits = getNativeEditReplacementsFromArgs(args);
377
- if (!rawPath || !edits) return undefined;
378
- return { path: formatDisplayPath(rawPath, options.cwd), edits };
379
- }
380
-
381
- function buildCursorEditActivityDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
382
- const rawPath = getCursorEditPathArg(args);
383
- return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
384
- }
385
-
386
- function formatNativeReadDisplayContent(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
387
- const value = asRecord(result.value);
388
- const totalLines = getNumber(value, "totalLines");
389
- const readOptions = {
390
- ...options,
391
- maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
392
- maxLines: options.maxLines ?? DEFAULT_NATIVE_READ_DISPLAY_LINES,
393
- };
394
- const content = getReadContent(args, result, readOptions);
395
- if (totalLines === undefined) return limitText(content, readOptions);
396
-
397
- const maxLines = readOptions.maxLines ?? DEFAULT_NATIVE_READ_DISPLAY_LINES;
398
- const lines = content.split("\n");
399
- const visible = lines.slice(0, maxLines).join("\n");
400
- if (totalLines <= maxLines && lines.length <= maxLines) return visible;
401
- if (visible.length > (readOptions.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS)) return limitText(content, readOptions, totalLines);
402
- return `${visible}\n\n[${Math.max(totalLines - maxLines, 0)} more lines in file. Use offset=${maxLines + 1} to continue.]`;
403
- }
404
-
405
- function getShellOutput(result: NormalizedResult, args: Record<string, unknown> = {}): { text: string; exitCode: number | undefined; timedOut: boolean } {
406
- const value = asRecord(result.value);
407
- const stdout = getString(value, "stdout") ?? "";
408
- const stderr = getString(value, "stderr") ?? "";
409
- const exitCode = getNumber(value, "exitCode");
410
- const timeoutMs = getNumber(args, "timeout");
411
- const executionTimeMs = getNumber(value, "executionTime");
412
- const timedOut = timeoutMs !== undefined && executionTimeMs !== undefined && executionTimeMs >= timeoutMs;
413
- const outputParts: string[] = [];
414
- if (stdout) outputParts.push(stdout.trimEnd());
415
- if (stderr) outputParts.push(stderr.trimEnd());
416
- if (exitCode !== undefined && exitCode !== 0) outputParts.push(`Command exited with code ${exitCode}`);
417
- if (timedOut) outputParts.push(`Command backgrounded after ${(timeoutMs / 1000).toFixed(0)} second timeout`);
418
- return { text: outputParts.filter(Boolean).join("\n\n") || "(no output)", exitCode, timedOut };
419
- }
420
-
421
- function buildShellDisplayArgs(args: Record<string, unknown>): Record<string, unknown> {
422
- const command = typeof args.command === "string" ? args.command : undefined;
423
- const timeoutMs = getNumber(args, "timeout");
424
- const displayArgs: Record<string, unknown> = command ? { command } : { ...args };
425
- if (timeoutMs !== undefined) {
426
- displayArgs.timeout = timeoutMs / 1000;
427
- }
428
- return displayArgs;
429
- }
430
-
431
- function formatShell(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
432
- const command = typeof args.command === "string" ? args.command : stringifyUnknown(args).trim();
433
- if (result.status === "error") return joinSections(`$ ${command || "shell"}`, formatError(result.error));
434
-
435
- const value = asRecord(result.value);
436
- const executionTime = getNumber(value, "executionTime");
437
- const outputParts = [getShellOutput(result, args).text];
438
- if (executionTime !== undefined) outputParts.push(`Took ${(executionTime / 1000).toFixed(1)}s`);
439
- return joinSections(`$ ${command || "shell"}`, limitText(outputParts.filter(Boolean).join("\n\n"), options));
440
- }
441
-
442
- function renderTreeNode(node: unknown, depth = 0, lines: string[] = []): string[] {
443
- const record = asRecord(node);
444
- if (!record) return lines;
445
- const name = getString(record, "name") ?? getString(record, "path") ?? getString(record, "relativePath") ?? "";
446
- const indent = " ".repeat(depth);
447
- if (name) lines.push(`${indent}${name}`);
448
- const children = getArray(record, "children") ?? getArray(record, "entries") ?? getArray(record, "files") ?? [];
449
- for (const child of children) renderTreeNode(child, depth + 1, lines);
450
- return lines;
451
- }
452
-
453
- function getLsBody(result: NormalizedResult, options: TranscriptOptions): string {
454
- const value = asRecord(result.value);
455
- const root = value?.directoryTreeRoot ?? result.value;
456
- const treeLines = renderTreeNode(root);
457
- const body = treeLines.length > 0 ? treeLines.join("\n") : stringifyUnknown(result.value);
458
- return limitText(body, options);
459
- }
460
-
461
- function formatLs(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
462
- const path = formatPathArg(args, options) ?? ".";
463
- if (result.status === "error") return joinSections(`ls ${path}`, formatError(result.error));
464
- return joinSections(`ls ${path}`, getLsBody(result, options));
465
- }
466
-
467
- function formatGlob(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
468
- const header = `$ ${synthesizeGlobBashCommand(args, options)}`;
469
- if (result.status === "error") return joinSections(header, formatError(result.error));
470
- return joinSections(header, getGlobBody(result, options));
471
- }
472
-
473
- function formatSearchCount(totalMatches: number): string {
474
- return totalMatches === 1 ? "1 match" : `${totalMatches} matches`;
475
- }
476
-
477
- function formatSearchFile(file: string): string {
478
- return file.endsWith(":") ? file.slice(0, -1) : file;
479
- }
480
-
481
- function collectSearchResults(value: unknown): string[] {
482
- const record = asRecord(value);
483
- const outputs: unknown[] = [];
484
- const activeEditorResult = record?.activeEditorResult;
485
- if (activeEditorResult) outputs.push(activeEditorResult);
486
- const workspaceResults = asRecord(record?.workspaceResults);
487
- if (workspaceResults) outputs.push(...Object.values(workspaceResults));
488
- if (outputs.length === 0) outputs.push(value);
489
-
490
- const lines: string[] = [];
491
- let sawExplicitNoMatches = false;
492
- for (const outputValue of outputs) {
493
- const outputRecord = asRecord(outputValue);
494
- const type = getString(outputRecord, "type");
495
- const output = getRecord(outputRecord, "output");
496
- if (type === "content") {
497
- const matches = getArray(output, "matches") ?? [];
498
- if (matches.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
499
- for (const match of matches) {
500
- const matchRecord = asRecord(match);
501
- const file = formatSearchFile(getString(matchRecord, "file") ?? "");
502
- const lineNumber = getNumber(matchRecord, "lineNumber");
503
- const line = getString(matchRecord, "line") ?? "";
504
- if (lineNumber === undefined && !line.trim()) {
505
- if (file) lines.push(file);
506
- continue;
507
- }
508
- const location = `${file}${lineNumber !== undefined ? `:${lineNumber}` : ""}`;
509
- lines.push(line ? `${location}: ${line}` : location);
510
- }
511
- } else if (type === "files") {
512
- const files = getArray(output, "files") ?? [];
513
- if (files.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
514
- lines.push(...files.filter((entry): entry is string => typeof entry === "string").map(formatSearchFile));
515
- } else if (type === "count") {
516
- const counts = getArray(output, "counts") ?? [];
517
- if (counts.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
518
- for (const count of counts) {
519
- const countRecord = asRecord(count);
520
- lines.push(`${getString(countRecord, "file") ?? ""}: ${getNumber(countRecord, "count") ?? 0}`.trim());
521
- }
522
- } else {
523
- const totalMatches = getNumber(outputRecord, "totalMatches");
524
- if (totalMatches !== undefined) {
525
- if (totalMatches === 0) {
526
- sawExplicitNoMatches = true;
527
- continue;
528
- }
529
- lines.push(formatSearchCount(totalMatches));
530
- continue;
531
- }
532
- lines.push(stringifyUnknown(outputValue));
533
- }
534
- }
535
-
536
- const topLevelTotalMatches = getNumber(record, "totalMatches");
537
- if (lines.length === 0 && topLevelTotalMatches !== undefined) {
538
- return topLevelTotalMatches === 0 ? ["(no matches)"] : [formatSearchCount(topLevelTotalMatches)];
539
- }
540
- if (lines.length === 0 && sawExplicitNoMatches) return ["(no matches)"];
541
- return lines.filter(Boolean);
542
- }
543
-
544
- function synthesizeGrepBashCommand(args: Record<string, unknown>, options: TranscriptOptions): string {
545
- const pattern = typeof args.pattern === "string" ? args.pattern : "";
546
- const path = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
547
- const glob = typeof args.glob === "string" ? args.glob : undefined;
548
- return ["grep", pattern && JSON.stringify(pattern), path ?? glob].filter(Boolean).join(" ");
549
- }
550
-
551
- function buildGrepDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
552
- const displayArgs: Record<string, unknown> = {};
553
- const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
554
- const path = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
555
- const glob = typeof args.glob === "string" ? args.glob : undefined;
556
- const ignoreCase = getBoolean(args, "caseInsensitive");
557
- const context = getNumber(args, "context") ?? getNumber(args, "contextBefore") ?? getNumber(args, "contextAfter");
558
- const limit = getNumber(args, "headLimit");
559
- if (pattern !== undefined) displayArgs.pattern = pattern;
560
- if (path !== undefined) displayArgs.path = path;
561
- if (glob !== undefined) displayArgs.glob = glob;
562
- if (ignoreCase !== undefined) displayArgs.ignoreCase = ignoreCase;
563
- if (context !== undefined) displayArgs.context = context;
564
- if (limit !== undefined) displayArgs.limit = limit;
565
- return Object.keys(displayArgs).length > 0 ? displayArgs : args;
566
- }
567
-
568
- function getGlobPattern(args: Record<string, unknown>): string {
569
- return typeof args.globPattern === "string" ? args.globPattern : typeof args.pattern === "string" ? args.pattern : "*";
570
- }
571
-
572
- function getGlobTargetDirectory(args: Record<string, unknown>, options: TranscriptOptions): string | undefined {
573
- const rawPath = typeof args.targetDirectory === "string" ? args.targetDirectory : typeof args.path === "string" ? args.path : undefined;
574
- return rawPath ? formatDisplayPath(rawPath, options.cwd) : undefined;
575
- }
576
-
577
- function synthesizeGlobBashCommand(args: Record<string, unknown>, options: TranscriptOptions): string {
578
- const pattern = getGlobPattern(args);
579
- const targetDirectory = getGlobTargetDirectory(args, options);
580
- return targetDirectory ? `glob ${pattern} in ${targetDirectory}` : `glob ${pattern}`;
581
- }
582
-
583
- function buildFindDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
584
- const displayArgs: Record<string, unknown> = { pattern: getGlobPattern(args) };
585
- const targetDirectory = getGlobTargetDirectory(args, options);
586
- const limit = getNumber(args, "limit") ?? getNumber(args, "headLimit");
587
- if (targetDirectory !== undefined) displayArgs.path = targetDirectory;
588
- if (limit !== undefined) displayArgs.limit = limit;
589
- return displayArgs;
590
- }
591
-
592
- function getGrepBody(result: NormalizedResult, options: TranscriptOptions): string {
593
- const lines = collectSearchResults(result.value);
594
- const limited = limitItems(lines, options);
595
- const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more matches truncated)` : limited.items.join("\n");
596
- return limitText(body || stringifyUnknown(result.value), options);
597
- }
598
-
599
- function getGlobBody(result: NormalizedResult, options: TranscriptOptions): string {
600
- const value = asRecord(result.value);
601
- const files = getArray(value, "files")?.filter((entry): entry is string => typeof entry === "string") ?? [];
602
- if (files.length === 0) {
603
- const totalMatches = getNumber(value, "totalMatches");
604
- const totalFiles = getNumber(value, "totalFiles");
605
- if (totalMatches === 0 || totalFiles === 0) return "No files found matching pattern";
606
- return stringifyUnknown(result.value);
607
- }
608
- const limited = limitItems(files, options);
609
- const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more files truncated)` : limited.items.join("\n");
610
- return limitText(body, options);
611
- }
612
-
613
- function formatGrep(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
614
- const header = `$ ${synthesizeGrepBashCommand(args, options)}`;
615
- if (result.status === "error") return joinSections(header, formatError(result.error));
616
- return joinSections(header, getGrepBody(result, options));
617
- }
618
-
619
- function getCursorWriteArgContent(args: Record<string, unknown>): string | undefined {
620
- return getString(args, "content") ?? getString(args, "fileContent") ?? getString(args, "contents");
621
- }
622
-
623
- function getCursorWriteRecordedContent(args: Record<string, unknown>, resultValue: Record<string, unknown> | undefined): string | undefined {
624
- return getCursorWriteArgContent(args) ?? getString(resultValue, "fileContentAfterWrite");
625
- }
626
-
627
- function formatWrite(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
628
- const path = formatPathArg(args, options) ?? "unknown";
629
- if (result.status === "error") return joinSections(`write ${path}`, formatError(result.error));
630
-
631
- const value = asRecord(result.value);
632
- const linesCreated = getNumber(value, "linesCreated");
633
- const fileSize = getNumber(value, "fileSize");
634
- const fileContentAfterWrite = getCursorWriteRecordedContent(args, value);
635
- const parts = [
636
- linesCreated !== undefined ? `Created ${linesCreated} lines` : undefined,
637
- fileSize !== undefined ? `File size: ${fileSize} bytes` : undefined,
638
- fileContentAfterWrite ? limitText(fileContentAfterWrite, options) : undefined,
639
- ].filter((part): part is string => Boolean(part));
640
- return joinSections(`write ${path}`, parts.join("\n\n") || stringifyUnknown(result.value));
641
- }
642
-
643
- function formatEdit(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
644
- const path = formatPathArg(args, options) ?? "unknown";
645
- if (result.status === "error") return joinSections(`edit ${path}`, formatError(result.error));
646
-
647
- const value = asRecord(result.value);
648
- const diff = formatDiffString(getString(value, "diffString") ?? getString(value, "diff") ?? getString(value, "unifiedDiff"), options);
649
- const linesAdded = getNumber(value, "linesAdded");
650
- const linesRemoved = getNumber(value, "linesRemoved");
651
- const stats = [
652
- linesAdded !== undefined ? `+${linesAdded}` : undefined,
653
- linesRemoved !== undefined ? `-${linesRemoved}` : undefined,
654
- ].filter(Boolean).join(" ");
655
- const body = [stats, diff ? limitText(diff, options) : undefined].filter((part): part is string => Boolean(part)).join("\n\n");
656
- return joinSections(`edit ${path}`, body || stringifyUnknown(result.value));
657
- }
658
-
659
- function formatDelete(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
660
- const path = formatPathArg(args, options) ?? "unknown";
661
- if (result.status === "error") return joinSections(`delete ${path}`, formatError(result.error));
662
- const value = asRecord(result.value);
663
- const fileSize = getNumber(value, "fileSize");
664
- return joinSections(`delete ${path}`, fileSize !== undefined ? `Deleted ${fileSize} bytes` : stringifyUnknown(result.value));
665
- }
666
-
667
- function getReadLintPaths(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string[] {
668
- const explicitPaths = Array.isArray(args.paths)
669
- ? args.paths.filter((entry): entry is string => typeof entry === "string")
670
- : typeof args.path === "string"
671
- ? [args.path]
672
- : [];
673
- const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
674
- .map((file) => getString(asRecord(file), "path"))
675
- .filter((entry): entry is string => Boolean(entry));
676
- return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
677
- }
678
-
679
- function getReadLintDiagnostics(result: NormalizedResult, options: TranscriptOptions): string[] {
680
- const value = asRecord(result.value);
681
- const files = getArray(value, "fileDiagnostics") ?? [];
682
- const lines: string[] = [];
683
- for (const file of files) {
684
- const fileRecord = asRecord(file);
685
- const pathValue = getString(fileRecord, "path");
686
- const path = pathValue ? formatDisplayPath(pathValue, options.cwd) : "unknown";
687
- const diagnostics = getArray(fileRecord, "diagnostics") ?? [];
688
- for (const diagnostic of diagnostics) {
689
- const diagnosticRecord = asRecord(diagnostic);
690
- const severity = getString(diagnosticRecord, "severity") ?? "diagnostic";
691
- const message = getString(diagnosticRecord, "message") ?? "";
692
- const source = getString(diagnosticRecord, "source");
693
- lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
694
- }
695
- }
696
- return lines;
697
- }
698
-
699
- function formatReadLints(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
700
- const paths = getReadLintPaths(args, result, options);
701
- const header = `readLints${paths.length > 0 ? ` ${paths.join(" ")}` : ""}`;
702
- if (result.status === "error") return joinSections(header, formatError(result.error));
703
-
704
- const lines = getReadLintDiagnostics(result, options);
705
- if (lines.length === 0 && paths.length > 0) return joinSections(header, `No diagnostics in ${paths.join(", ")}`);
706
- return joinSections(header, limitText(lines.join("\n") || stringifyUnknown(result.value), options));
707
- }
708
-
709
- function getTodoItems(args: Record<string, unknown>, result: NormalizedResult): Array<{ content: string; status?: string }> {
710
- const value = asRecord(result.value);
711
- const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
712
- const todos: Array<{ content: string; status?: string }> = [];
713
- for (const todo of rawTodos) {
714
- const record = asRecord(todo);
715
- const content = getString(record, "content");
716
- if (!content) continue;
717
- const status = getString(record, "status");
718
- todos.push(status ? { content, status } : { content });
719
- }
720
- return todos;
721
- }
722
-
723
- function getTodoTotalCount(args: Record<string, unknown>, result: NormalizedResult, todos: Array<{ content: string; status?: string }>): number {
724
- return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
725
- }
726
-
727
- function summarizeTodos(args: Record<string, unknown>, result: NormalizedResult): string {
728
- const todos = getTodoItems(args, result);
729
- const total = getTodoTotalCount(args, result, todos);
730
- const completed = todos.filter((todo) => todo.status === "completed").length;
731
- const inProgress = todos.filter((todo) => todo.status === "inProgress").length;
732
- const pending = todos.filter((todo) => todo.status === "pending").length;
733
- const parts = [`${completed}/${total} completed`];
734
- if (inProgress > 0) parts.push(`${inProgress} in progress`);
735
- if (pending > 0) parts.push(`${pending} pending`);
736
- return parts.join(", ");
737
- }
738
-
739
- function formatTodoStatus(status: string | undefined): string {
740
- if (status === "completed") return "✓";
741
- if (status === "inProgress") return "…";
742
- if (status === "pending") return "○";
743
- return "•";
744
- }
745
-
746
- function formatTodos(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions, header: string): string {
747
- if (result.status === "error") return joinSections(header, formatError(result.error));
748
- const todos = getTodoItems(args, result);
749
- if (todos.length === 0) return joinSections(header, limitText(stringifyUnknown(result.value), options));
750
- const lines = todos.map((todo) => `${formatTodoStatus(todo.status)} ${todo.content}${todo.status ? ` (${todo.status})` : ""}`);
751
- return joinSections(header, limitText(lines.join("\n"), options));
752
- }
1
+ import {
2
+ asRecord,
3
+ getString,
4
+ getToolArgs,
5
+ getToolName,
6
+ getToolResult,
7
+ normalizeResult,
8
+ normalizeToolName,
9
+ type CursorPiToolDisplay,
10
+ type TranscriptOptions,
11
+ } from "./cursor-transcript-utils.js";
12
+ import {
13
+ buildCursorPiToolDisplayFromSpec,
14
+ formatCursorToolTranscriptFromSpec,
15
+ type ToolDisplayContext,
16
+ } from "./cursor-transcript-tool-specs.js";
17
+
18
+ export type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
753
19
 
754
20
  export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
755
21
  const name = normalizeToolName(getToolName(toolCall));
@@ -761,490 +27,23 @@ export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
761
27
  return trimmed || undefined;
762
28
  }
763
29
 
764
- function summarizePlan(args: Record<string, unknown>, result: NormalizedResult): string {
765
- const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
766
- const firstLine = planText ? firstNonEmptyLine(planText) : undefined;
767
- return firstLine ? truncateArg(firstLine, 160) : summarizeTodos(args, result);
768
- }
769
-
770
- function formatPlan(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
771
- if (result.status === "error") return joinSections("createPlan", formatError(result.error));
772
- const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
773
- if (planText?.trim()) return joinSections("createPlan", limitText(planText, options));
774
- return formatTodos(args, result, options, "createPlan");
775
- }
776
-
777
- function getTaskDescription(args: Record<string, unknown>, result: NormalizedResult): string {
778
- return getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
779
- }
780
-
781
- function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: string[]): Record<string, unknown> | undefined {
782
- let current = record;
783
- for (const key of keys) {
784
- current = getRecord(current, key);
785
- if (!current) return undefined;
786
- }
787
- return current;
788
- }
789
-
790
- function collectTaskText(result: NormalizedResult): string {
791
- const value = asRecord(result.value);
792
- const success = getNestedRecord(value, "result", "success");
793
- const command = getString(success, "command");
794
- const stdout = getString(success, "stdout");
795
- const interleavedOutput = getString(success, "interleavedOutput");
796
- const assistantMessages = (getArray(value, "conversationSteps") ?? [])
797
- .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
798
- .filter((entry): entry is string => Boolean(entry));
799
- const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
800
- return parts.join("\n");
801
- }
802
-
803
- function formatTask(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
804
- const description = getTaskDescription(args, result);
805
- if (result.status === "error") return joinSections(`task ${description}`, formatError(result.error));
806
- const taskText = collectTaskText(result);
807
- return joinSections(`task ${description}`, limitText(taskText || stringifyUnknown(result.value), options));
808
- }
809
-
810
- function summarizeTask(description: string, taskText: string): string {
811
- const firstLine = firstNonEmptyLine(taskText);
812
- if (!firstLine) return truncateArg(description);
813
- if (description === "task" || description === firstLine) return truncateArg(firstLine);
814
- return truncateArg(`${description}: ${firstLine}`, 160);
815
- }
816
-
817
- function getGenerateImageValue(result: NormalizedResult): Record<string, unknown> | undefined {
818
- return asRecord(result.value);
819
- }
820
-
821
- function getGenerateImagePath(args: Record<string, unknown>, result: NormalizedResult): string | undefined {
822
- const value = getGenerateImageValue(result);
823
- return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
824
- }
825
-
826
- function getGenerateImageDisplayPath(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string | undefined {
827
- const path = getGenerateImagePath(args, result);
828
- return path ? formatDisplayPath(path, options.cwd) : undefined;
829
- }
830
-
831
- function inferImageMimeType(path: string | undefined): string | undefined {
832
- const lower = path?.toLowerCase();
833
- if (!lower) return undefined;
834
- if (lower.endsWith(".png")) return "image/png";
835
- if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
836
- if (lower.endsWith(".gif")) return "image/gif";
837
- if (lower.endsWith(".webp")) return "image/webp";
838
- return undefined;
839
- }
840
-
841
- function formatGenerateImage(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
842
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
843
- if (result.status === "error") return joinSections(`generateImage ${prompt}`, formatError(result.error));
844
- const value = getGenerateImageValue(result);
845
- const displayPath = getGenerateImageDisplayPath(args, result, options);
846
- const hasImageData = typeof value?.imageData === "string" && value.imageData.length > 0;
847
- const lines = [displayPath ? `Saved image: ${displayPath}` : undefined, hasImageData ? "Image data returned by Cursor SDK." : undefined].filter(
848
- (line): line is string => Boolean(line),
849
- );
850
- if (lines.length > 0) return joinSections(`generateImage ${prompt}`, lines.join("\n"));
851
- return joinSections(`generateImage ${prompt}`, limitText(stringifyUnknown(result.value), options));
852
- }
853
-
854
- function getMcpContentText(entry: unknown): string | undefined {
855
- const record = asRecord(entry);
856
- const directText = getString(record, "text");
857
- if (directText) return directText;
858
- const nestedText = getRecord(record, "text");
859
- return getString(nestedText, "text");
860
- }
861
-
862
- function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
863
- const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
864
- if (result.status === "error") return joinSections(toolName, formatError(result.error));
865
-
866
- const value = asRecord(result.value);
867
- const isError = getBoolean(value, "isError");
868
- const content = getArray(value, "content") ?? [];
869
- const text = content
870
- .map((entry) => getMcpContentText(entry))
871
- .filter((entry): entry is string => Boolean(entry))
872
- .join("\n");
873
- const body = `${isError ? "[tool error]\n" : ""}${text || stringifyUnknown(result.value)}`;
874
- return joinSections(toolName, limitText(body, options));
875
- }
876
-
877
- function formatFallback(name: string, args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
878
- const header = name === "unknown" ? "Cursor tool" : name;
879
- if (result.status === "error") return joinSections(header, formatError(result.error));
880
- const argsText = Object.keys(args).length > 0 ? `${stringifyUnknown(args)}\n\n` : "";
881
- return joinSections(header, limitText(`${argsText}${stringifyUnknown(result.value)}`.trim(), options));
882
- }
883
-
884
- export function formatCursorToolTranscript(toolCall: unknown, options: TranscriptOptions = {}): string {
885
- const name = normalizeToolName(getToolName(toolCall));
886
- const args = getToolArgs(toolCall);
887
- const result = normalizeResult(getToolResult(toolCall));
888
-
889
- switch (name) {
890
- case "read":
891
- return formatRead(args, result, options);
892
- case "shell":
893
- return formatShell(args, result, options);
894
- case "ls":
895
- return formatLs(args, result, options);
896
- case "glob":
897
- return formatGlob(args, result, options);
898
- case "grep":
899
- return formatGrep(args, result, options);
900
- case "write":
901
- return formatWrite(args, result, options);
902
- case "edit":
903
- return formatEdit(args, result, options);
904
- case "delete":
905
- return formatDelete(args, result, options);
906
- case "readLints":
907
- return formatReadLints(args, result, options);
908
- case "updateTodos":
909
- return formatTodos(args, result, options, "updateTodos");
910
- case "createPlan":
911
- return formatPlan(args, result, options);
912
- case "task":
913
- return formatTask(args, result, options);
914
- case "generateImage":
915
- return formatGenerateImage(args, result, options);
916
- case "mcp":
917
- return formatMcp(args, result, options);
918
- default:
919
- return formatFallback(name, args, result, options);
920
- }
921
- }
922
-
923
- function textToolResult(text: string, details?: unknown): PiToolDisplayResult {
924
- return { content: [{ type: "text", text }], details };
925
- }
926
-
927
- function buildGenericPiToolDisplay(name: string, args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): CursorPiToolDisplay {
928
- const isError = result.status === "error";
929
- return {
930
- toolName: name,
931
- args,
932
- result: textToolResult(isError ? formatError(result.error) : limitText(stringifyUnknown(result.value), options)),
933
- isError,
934
- };
935
- }
936
-
937
- function firstNonEmptyLine(text: string): string | undefined {
938
- return text.split("\n").find((line) => line.trim())?.trim();
939
- }
940
-
941
- function buildReplaySummaryDisplay(
942
- toolName: string,
943
- args: Record<string, unknown>,
944
- result: NormalizedResult,
945
- contentText: string,
946
- details: Record<string, unknown>,
947
- ): CursorPiToolDisplay {
948
- const isError = result.status === "error";
949
- const summary = isError ? formatError(result.error) : firstNonEmptyLine(contentText);
30
+ function buildToolDisplayContext(toolCall: unknown, options: TranscriptOptions): ToolDisplayContext {
31
+ const rawName = getToolName(toolCall);
950
32
  return {
951
- toolName,
952
- args,
953
- result: textToolResult(contentText, {
954
- ...details,
955
- summary: details.summary ?? summary,
956
- expandedText: details.expandedText ?? contentText,
957
- }),
958
- isError,
33
+ rawName,
34
+ name: normalizeToolName(rawName),
35
+ args: getToolArgs(toolCall),
36
+ result: normalizeResult(getToolResult(toolCall)),
37
+ options,
959
38
  };
960
39
  }
961
40
 
962
- function truncateArg(value: string, maxLength = 120): string {
963
- return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
964
- }
965
-
966
- function buildCursorActivityDisplayArgs(
967
- args: Record<string, unknown>,
968
- activityTitle: string,
969
- activitySummary: string | undefined,
970
- ): Record<string, unknown> {
971
- const trimmedSummary = activitySummary?.trim();
972
- return {
973
- ...args,
974
- activityTitle,
975
- ...(trimmedSummary ? { activitySummary: trimmedSummary } : {}),
976
- };
41
+ export function formatCursorToolTranscript(toolCall: unknown, options: TranscriptOptions = {}): string {
42
+ return formatCursorToolTranscriptFromSpec(buildToolDisplayContext(toolCall, options));
977
43
  }
978
44
 
979
45
  export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptOptions = {}): CursorPiToolDisplay {
980
- const rawName = getToolName(toolCall);
981
- const name = normalizeToolName(rawName);
982
- const args = getToolArgs(toolCall);
983
- const result = normalizeResult(getToolResult(toolCall));
984
-
985
- if (name === "read") {
986
- const isError = result.status === "error";
987
- return {
988
- toolName: "read",
989
- args: buildReadDisplayArgs(args, options),
990
- result: textToolResult(isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options)),
991
- isError,
992
- };
993
- }
994
-
995
- if (name === "shell") {
996
- const shellOutput = getShellOutput(result, args);
997
- const isError = result.status === "error" || shellOutput.timedOut || (shellOutput.exitCode !== undefined && shellOutput.exitCode !== 0);
998
- return {
999
- toolName: "bash",
1000
- args: buildShellDisplayArgs(args),
1001
- result: textToolResult(result.status === "error" ? formatError(result.error) : limitText(shellOutput.text, options)),
1002
- isError,
1003
- };
1004
- }
1005
-
1006
- if (name === "grep") {
1007
- const isError = result.status === "error";
1008
- const outputText = isError ? formatError(result.error) : getGrepBody(result, options);
1009
- return {
1010
- toolName: "grep",
1011
- args: buildGrepDisplayArgs(args, options),
1012
- result: textToolResult(outputText),
1013
- isError,
1014
- };
1015
- }
1016
-
1017
- if (name === "glob") {
1018
- const isError = result.status === "error";
1019
- return {
1020
- toolName: "find",
1021
- args: buildFindDisplayArgs(args, options),
1022
- result: textToolResult(isError ? formatError(result.error) : getGlobBody(result, options)),
1023
- isError,
1024
- };
1025
- }
1026
-
1027
- if (name === "ls") {
1028
- return {
1029
- toolName: "ls",
1030
- args,
1031
- result: textToolResult(result.status === "error" ? formatError(result.error) : getLsBody(result, options).trim()),
1032
- isError: result.status === "error",
1033
- };
1034
- }
1035
-
1036
- if (name === "edit") {
1037
- const value = asRecord(result.value);
1038
- const rawDiff = getString(value, "diffString") ?? getString(value, "diff") ?? getString(value, "unifiedDiff");
1039
- const normalizedDiff = formatDiffString(rawDiff, options);
1040
- const nativeEditArgs = buildNativeEditDisplayArgs(rawName, args, options);
1041
- const baseActivityArgs = buildCursorEditActivityDisplayArgs(args, options);
1042
- const displayPath = typeof baseActivityArgs.path === "string" ? baseActivityArgs.path : undefined;
1043
- const activityTitle = getCursorReplayDisplayLabel("cursor_edit");
1044
- const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
1045
- const contentText = formatEdit(activityArgs, result, options);
1046
- const details = {
1047
- cursorToolName: "edit",
1048
- path: displayPath,
1049
- linesAdded: getNumber(value, "linesAdded"),
1050
- linesRemoved: getNumber(value, "linesRemoved"),
1051
- diffString: normalizedDiff,
1052
- diff: normalizedDiff,
1053
- firstChangedLine: getNumber(value, "firstChangedLine"),
1054
- };
1055
- if (nativeEditArgs) {
1056
- return {
1057
- toolName: "edit",
1058
- args: nativeEditArgs,
1059
- result: textToolResult(contentText, details),
1060
- isError: result.status === "error",
1061
- };
1062
- }
1063
- return buildReplaySummaryDisplay(
1064
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1065
- activityArgs,
1066
- result,
1067
- contentText.trimEnd(),
1068
- {
1069
- ...details,
1070
- title: activityTitle,
1071
- summary: result.status === "error" ? undefined : displayPath ?? "replayed",
1072
- },
1073
- );
1074
- }
1075
-
1076
- if (name === "write") {
1077
- const value = asRecord(result.value);
1078
- const content = getCursorWriteArgContent(args);
1079
- const displayArgs = buildWriteDisplayArgs(args, options);
1080
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
1081
- const contentText = formatWrite(args, result, options).trimEnd();
1082
- const details = {
1083
- cursorToolName: "write",
1084
- path: displayPath,
1085
- linesCreated: getNumber(value, "linesCreated"),
1086
- fileSize: getNumber(value, "fileSize"),
1087
- fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
1088
- expandedText: contentText,
1089
- };
1090
- if (content === undefined) {
1091
- const activityTitle = getCursorReplayDisplayLabel("cursor_write");
1092
- return buildReplaySummaryDisplay(
1093
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1094
- buildCursorActivityDisplayArgs(displayArgs, activityTitle, displayPath ?? "file"),
1095
- result,
1096
- contentText,
1097
- {
1098
- ...details,
1099
- title: activityTitle,
1100
- summary: result.status === "error" ? undefined : displayPath ?? "wrote file",
1101
- },
1102
- );
1103
- }
1104
- return {
1105
- toolName: "write",
1106
- args: displayArgs,
1107
- result: textToolResult(contentText, details),
1108
- isError: result.status === "error",
1109
- };
1110
- }
1111
-
1112
- if (name === "delete") {
1113
- const value = asRecord(result.value);
1114
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
1115
- const activityTitle = getCursorReplayDisplayLabel("cursor_delete");
1116
- const contentText = formatDelete(args, result, options).trimEnd();
1117
- return buildReplaySummaryDisplay(
1118
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1119
- buildCursorActivityDisplayArgs(displayPath ? { path: displayPath } : {}, activityTitle, displayPath ?? "file"),
1120
- result,
1121
- contentText,
1122
- {
1123
- cursorToolName: "delete",
1124
- title: activityTitle,
1125
- path: displayPath,
1126
- summary: result.status === "error" ? undefined : displayPath ? `deleted ${displayPath}` : "deleted file",
1127
- fileSize: getNumber(value, "fileSize"),
1128
- },
1129
- );
1130
- }
1131
-
1132
- if (name === "readLints") {
1133
- const paths = getReadLintPaths(args, result, options);
1134
- const diagnosticCount = getReadLintDiagnostics(result, options).length;
1135
- const activityTitle = getCursorReplayDisplayLabel("cursor_read_lints");
1136
- const diagnosticSummary = `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}${paths.length > 0 ? ` in ${paths.join(", ")}` : ""}`;
1137
- const contentText = formatReadLints(args, result, options).trimEnd();
1138
- return buildReplaySummaryDisplay(
1139
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1140
- buildCursorActivityDisplayArgs({ paths, diagnosticCount }, activityTitle, diagnosticSummary),
1141
- result,
1142
- contentText,
1143
- {
1144
- cursorToolName: "readLints",
1145
- title: activityTitle,
1146
- summary: result.status === "error" ? undefined : diagnosticSummary,
1147
- },
1148
- );
1149
- }
1150
-
1151
- if (name === "updateTodos") {
1152
- const todos = getTodoItems(args, result);
1153
- const totalCount = getTodoTotalCount(args, result, todos);
1154
- const activityTitle = getCursorReplayDisplayLabel("cursor_update_todos");
1155
- const todoSummary = summarizeTodos(args, result);
1156
- const contentText = formatTodos(args, result, options, "updateTodos").trimEnd();
1157
- return buildReplaySummaryDisplay(
1158
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1159
- buildCursorActivityDisplayArgs({ totalCount }, activityTitle, todoSummary),
1160
- result,
1161
- contentText,
1162
- {
1163
- cursorToolName: "updateTodos",
1164
- title: activityTitle,
1165
- summary: result.status === "error" ? undefined : todoSummary,
1166
- },
1167
- );
1168
- }
1169
-
1170
- if (name === "createPlan") {
1171
- const todos = getTodoItems(args, result);
1172
- const totalCount = getTodoTotalCount(args, result, todos);
1173
- const activityTitle = getCursorReplayDisplayLabel("cursor_create_plan");
1174
- const planSummary = summarizePlan(args, result);
1175
- const contentText = formatPlan(args, result, options).trimEnd();
1176
- return buildReplaySummaryDisplay(
1177
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1178
- buildCursorActivityDisplayArgs({ totalCount }, activityTitle, planSummary),
1179
- result,
1180
- contentText,
1181
- {
1182
- cursorToolName: "createPlan",
1183
- title: activityTitle,
1184
- summary: result.status === "error" ? undefined : planSummary,
1185
- },
1186
- );
1187
- }
1188
-
1189
- if (name === "task") {
1190
- const description = getTaskDescription(args, result);
1191
- const contentText = formatTask(args, result, options).trimEnd();
1192
- const taskText = collectTaskText(result);
1193
- const activityTitle = getCursorReplayDisplayLabel("cursor_task");
1194
- const taskSummary = summarizeTask(description, taskText);
1195
- return buildReplaySummaryDisplay(
1196
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1197
- buildCursorActivityDisplayArgs({ description: truncateArg(description) }, activityTitle, taskSummary),
1198
- result,
1199
- contentText,
1200
- {
1201
- cursorToolName: "task",
1202
- title: activityTitle,
1203
- summary: result.status === "error" ? undefined : taskSummary,
1204
- },
1205
- );
1206
- }
1207
-
1208
- if (name === "generateImage") {
1209
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
1210
- const contentText = formatGenerateImage(args, result, options).trimEnd();
1211
- const imagePath = getGenerateImagePath(args, result);
1212
- const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
1213
- const activityTitle = getCursorReplayDisplayLabel("cursor_generate_image");
1214
- return buildReplaySummaryDisplay(
1215
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1216
- buildCursorActivityDisplayArgs({ prompt: truncateArg(prompt) }, activityTitle, imageDisplayPath ?? truncateArg(prompt)),
1217
- result,
1218
- contentText,
1219
- {
1220
- cursorToolName: "generateImage",
1221
- title: activityTitle,
1222
- summary: result.status === "error" ? undefined : imageDisplayPath ? `saved ${imageDisplayPath}` : "image generated",
1223
- imagePath,
1224
- imageDisplayPath,
1225
- imageMimeType: inferImageMimeType(imagePath),
1226
- },
1227
- );
1228
- }
1229
-
1230
- if (name === "mcp") {
1231
- const toolName = getString(args, "toolName") ?? "mcp";
1232
- const activityTitle = getCursorReplayDisplayLabel("cursor_mcp");
1233
- const contentText = formatMcp(args, result, options).trimEnd();
1234
- return buildReplaySummaryDisplay(
1235
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1236
- buildCursorActivityDisplayArgs({ toolName: truncateArg(toolName) }, activityTitle, truncateArg(toolName)),
1237
- result,
1238
- contentText,
1239
- {
1240
- cursorToolName: "mcp",
1241
- title: activityTitle,
1242
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "MCP result captured",
1243
- },
1244
- );
1245
- }
1246
-
1247
- return buildGenericPiToolDisplay(name, args, result, options);
46
+ return buildCursorPiToolDisplayFromSpec(buildToolDisplayContext(toolCall, options));
1248
47
  }
1249
48
 
1250
49
  export function mergeCursorToolCalls(startedToolCall: unknown, completedToolCall: unknown): unknown {