pi-cursor-sdk 0.1.13 → 0.1.15
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.
- package/CHANGELOG.md +36 -0
- package/README.md +71 -32
- package/docs/cursor-model-ux-spec.md +23 -9
- package/docs/cursor-native-tool-replay.md +88 -0
- package/docs/cursor-native-tool-visual-audit.md +183 -0
- package/package.json +5 -2
- package/src/bundled-context-windows.ts +5 -2
- package/src/context.ts +34 -11
- package/src/cursor-fallback-models.generated.ts +4068 -71
- package/src/cursor-mcp-timeout-override.ts +111 -0
- package/src/cursor-native-tool-display.ts +397 -46
- package/src/cursor-pi-tool-bridge.ts +637 -0
- package/src/cursor-provider.ts +477 -81
- package/src/cursor-question-tool.ts +247 -0
- package/src/cursor-session-cwd.ts +33 -0
- package/src/cursor-tool-names.ts +67 -0
- package/src/cursor-tool-transcript.ts +730 -61
- package/src/index.ts +7 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayDisplayLabel } from "./cursor-tool-names.js";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
|
|
5
6
|
const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
|
|
6
7
|
const DEFAULT_MAX_LIST_ITEMS = 200;
|
|
7
8
|
const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
|
|
8
9
|
const DEFAULT_READ_TRANSCRIPT_LINES = 12;
|
|
10
|
+
const DEFAULT_NATIVE_READ_DISPLAY_LINES = 20;
|
|
9
11
|
const LOCAL_READ_PREVIEW_NOTICE =
|
|
10
12
|
"[local file preview at transcript time; Cursor read result content was unavailable]";
|
|
11
13
|
|
|
@@ -79,7 +81,8 @@ function getToolResult(toolCall: unknown): unknown {
|
|
|
79
81
|
|
|
80
82
|
function normalizeToolName(name: string): string {
|
|
81
83
|
const normalized = name.replace(/\s+/g, " ").trim();
|
|
82
|
-
|
|
84
|
+
const normalizedKey = normalized.toLowerCase();
|
|
85
|
+
switch (normalizedKey) {
|
|
83
86
|
case "read_file":
|
|
84
87
|
return "read";
|
|
85
88
|
case "list_dir":
|
|
@@ -87,12 +90,26 @@ function normalizeToolName(name: string): string {
|
|
|
87
90
|
case "run_terminal_cmd":
|
|
88
91
|
case "terminal":
|
|
89
92
|
case "bash":
|
|
93
|
+
case "shell":
|
|
90
94
|
return "shell";
|
|
91
95
|
case "grep_search":
|
|
92
96
|
case "search":
|
|
93
97
|
return "grep";
|
|
94
98
|
case "file_search":
|
|
95
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";
|
|
96
113
|
default:
|
|
97
114
|
return normalized || "unknown";
|
|
98
115
|
}
|
|
@@ -167,6 +184,27 @@ function formatDisplayPath(path: string, cwd = process.cwd()): string {
|
|
|
167
184
|
return relativePath;
|
|
168
185
|
}
|
|
169
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
|
+
|
|
170
208
|
function resolveFilePath(path: string, cwd = process.cwd()): string {
|
|
171
209
|
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
172
210
|
}
|
|
@@ -255,16 +293,139 @@ function formatRead(args: Record<string, unknown>, result: NormalizedResult, opt
|
|
|
255
293
|
return joinSections(`read ${path}`, limitText(getReadContent(args, result, options), readOptions, totalLines));
|
|
256
294
|
}
|
|
257
295
|
|
|
258
|
-
function
|
|
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 } {
|
|
259
406
|
const value = asRecord(result.value);
|
|
260
407
|
const stdout = getString(value, "stdout") ?? "";
|
|
261
408
|
const stderr = getString(value, "stderr") ?? "";
|
|
262
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;
|
|
263
413
|
const outputParts: string[] = [];
|
|
264
414
|
if (stdout) outputParts.push(stdout.trimEnd());
|
|
265
415
|
if (stderr) outputParts.push(stderr.trimEnd());
|
|
266
416
|
if (exitCode !== undefined && exitCode !== 0) outputParts.push(`Command exited with code ${exitCode}`);
|
|
267
|
-
|
|
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;
|
|
268
429
|
}
|
|
269
430
|
|
|
270
431
|
function formatShell(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
@@ -273,7 +434,7 @@ function formatShell(args: Record<string, unknown>, result: NormalizedResult, op
|
|
|
273
434
|
|
|
274
435
|
const value = asRecord(result.value);
|
|
275
436
|
const executionTime = getNumber(value, "executionTime");
|
|
276
|
-
const outputParts = [getShellOutput(result).text];
|
|
437
|
+
const outputParts = [getShellOutput(result, args).text];
|
|
277
438
|
if (executionTime !== undefined) outputParts.push(`Took ${(executionTime / 1000).toFixed(1)}s`);
|
|
278
439
|
return joinSections(`$ ${command || "shell"}`, limitText(outputParts.filter(Boolean).join("\n\n"), options));
|
|
279
440
|
}
|
|
@@ -304,17 +465,17 @@ function formatLs(args: Record<string, unknown>, result: NormalizedResult, optio
|
|
|
304
465
|
}
|
|
305
466
|
|
|
306
467
|
function formatGlob(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
307
|
-
const
|
|
308
|
-
const targetDirectory = typeof args.targetDirectory === "string" ? formatDisplayPath(args.targetDirectory, options.cwd) : undefined;
|
|
309
|
-
const header = targetDirectory ? `glob ${pattern} in ${targetDirectory}` : `glob ${pattern}`;
|
|
468
|
+
const header = `$ ${synthesizeGlobBashCommand(args, options)}`;
|
|
310
469
|
if (result.status === "error") return joinSections(header, formatError(result.error));
|
|
470
|
+
return joinSections(header, getGlobBody(result, options));
|
|
471
|
+
}
|
|
311
472
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return
|
|
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;
|
|
318
479
|
}
|
|
319
480
|
|
|
320
481
|
function collectSearchResults(value: unknown): string[] {
|
|
@@ -327,46 +488,140 @@ function collectSearchResults(value: unknown): string[] {
|
|
|
327
488
|
if (outputs.length === 0) outputs.push(value);
|
|
328
489
|
|
|
329
490
|
const lines: string[] = [];
|
|
491
|
+
let sawExplicitNoMatches = false;
|
|
330
492
|
for (const outputValue of outputs) {
|
|
331
493
|
const outputRecord = asRecord(outputValue);
|
|
332
494
|
const type = getString(outputRecord, "type");
|
|
333
495
|
const output = getRecord(outputRecord, "output");
|
|
334
496
|
if (type === "content") {
|
|
335
497
|
const matches = getArray(output, "matches") ?? [];
|
|
498
|
+
if (matches.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
|
|
336
499
|
for (const match of matches) {
|
|
337
500
|
const matchRecord = asRecord(match);
|
|
338
|
-
const file = getString(matchRecord, "file") ?? "";
|
|
501
|
+
const file = formatSearchFile(getString(matchRecord, "file") ?? "");
|
|
339
502
|
const lineNumber = getNumber(matchRecord, "lineNumber");
|
|
340
503
|
const line = getString(matchRecord, "line") ?? "";
|
|
341
|
-
|
|
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);
|
|
342
510
|
}
|
|
343
511
|
} else if (type === "files") {
|
|
344
512
|
const files = getArray(output, "files") ?? [];
|
|
345
|
-
|
|
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));
|
|
346
515
|
} else if (type === "count") {
|
|
347
516
|
const counts = getArray(output, "counts") ?? [];
|
|
517
|
+
if (counts.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
|
|
348
518
|
for (const count of counts) {
|
|
349
519
|
const countRecord = asRecord(count);
|
|
350
520
|
lines.push(`${getString(countRecord, "file") ?? ""}: ${getNumber(countRecord, "count") ?? 0}`.trim());
|
|
351
521
|
}
|
|
352
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
|
+
}
|
|
353
532
|
lines.push(stringifyUnknown(outputValue));
|
|
354
533
|
}
|
|
355
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)"];
|
|
356
541
|
return lines.filter(Boolean);
|
|
357
542
|
}
|
|
358
543
|
|
|
359
|
-
function
|
|
544
|
+
function synthesizeGrepBashCommand(args: Record<string, unknown>, options: TranscriptOptions): string {
|
|
360
545
|
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
361
546
|
const path = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
|
|
362
547
|
const glob = typeof args.glob === "string" ? args.glob : undefined;
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
591
|
|
|
592
|
+
function getGrepBody(result: NormalizedResult, options: TranscriptOptions): string {
|
|
366
593
|
const lines = collectSearchResults(result.value);
|
|
367
594
|
const limited = limitItems(lines, options);
|
|
368
595
|
const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more matches truncated)` : limited.items.join("\n");
|
|
369
|
-
return
|
|
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");
|
|
370
625
|
}
|
|
371
626
|
|
|
372
627
|
function formatWrite(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
@@ -376,7 +631,7 @@ function formatWrite(args: Record<string, unknown>, result: NormalizedResult, op
|
|
|
376
631
|
const value = asRecord(result.value);
|
|
377
632
|
const linesCreated = getNumber(value, "linesCreated");
|
|
378
633
|
const fileSize = getNumber(value, "fileSize");
|
|
379
|
-
const fileContentAfterWrite =
|
|
634
|
+
const fileContentAfterWrite = getCursorWriteRecordedContent(args, value);
|
|
380
635
|
const parts = [
|
|
381
636
|
linesCreated !== undefined ? `Created ${linesCreated} lines` : undefined,
|
|
382
637
|
fileSize !== undefined ? `File size: ${fileSize} bytes` : undefined,
|
|
@@ -390,7 +645,7 @@ function formatEdit(args: Record<string, unknown>, result: NormalizedResult, opt
|
|
|
390
645
|
if (result.status === "error") return joinSections(`edit ${path}`, formatError(result.error));
|
|
391
646
|
|
|
392
647
|
const value = asRecord(result.value);
|
|
393
|
-
const diff = getString(value, "diffString");
|
|
648
|
+
const diff = formatDiffString(getString(value, "diffString") ?? getString(value, "diff") ?? getString(value, "unifiedDiff"), options);
|
|
394
649
|
const linesAdded = getNumber(value, "linesAdded");
|
|
395
650
|
const linesRemoved = getNumber(value, "linesRemoved");
|
|
396
651
|
const stats = [
|
|
@@ -409,13 +664,19 @@ function formatDelete(args: Record<string, unknown>, result: NormalizedResult, o
|
|
|
409
664
|
return joinSections(`delete ${path}`, fileSize !== undefined ? `Deleted ${fileSize} bytes` : stringifyUnknown(result.value));
|
|
410
665
|
}
|
|
411
666
|
|
|
412
|
-
function
|
|
413
|
-
const
|
|
414
|
-
? args.paths.filter((entry): entry is string => typeof entry === "string")
|
|
415
|
-
:
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
}
|
|
418
678
|
|
|
679
|
+
function getReadLintDiagnostics(result: NormalizedResult, options: TranscriptOptions): string[] {
|
|
419
680
|
const value = asRecord(result.value);
|
|
420
681
|
const files = getArray(value, "fileDiagnostics") ?? [];
|
|
421
682
|
const lines: string[] = [];
|
|
@@ -432,9 +693,172 @@ function formatReadLints(args: Record<string, unknown>, result: NormalizedResult
|
|
|
432
693
|
lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
|
|
433
694
|
}
|
|
434
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(", ")}`);
|
|
435
706
|
return joinSections(header, limitText(lines.join("\n") || stringifyUnknown(result.value), options));
|
|
436
707
|
}
|
|
437
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
|
+
}
|
|
753
|
+
|
|
754
|
+
export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
|
|
755
|
+
const name = normalizeToolName(getToolName(toolCall));
|
|
756
|
+
if (name !== "createPlan") return undefined;
|
|
757
|
+
const args = getToolArgs(toolCall);
|
|
758
|
+
const result = normalizeResult(getToolResult(toolCall));
|
|
759
|
+
const plan = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
|
|
760
|
+
const trimmed = plan?.trim();
|
|
761
|
+
return trimmed || undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
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
|
+
|
|
438
862
|
function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
439
863
|
const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
|
|
440
864
|
if (result.status === "error") return joinSections(toolName, formatError(result.error));
|
|
@@ -443,7 +867,7 @@ function formatMcp(args: Record<string, unknown>, result: NormalizedResult, opti
|
|
|
443
867
|
const isError = getBoolean(value, "isError");
|
|
444
868
|
const content = getArray(value, "content") ?? [];
|
|
445
869
|
const text = content
|
|
446
|
-
.map((entry) =>
|
|
870
|
+
.map((entry) => getMcpContentText(entry))
|
|
447
871
|
.filter((entry): entry is string => Boolean(entry))
|
|
448
872
|
.join("\n");
|
|
449
873
|
const body = `${isError ? "[tool error]\n" : ""}${text || stringifyUnknown(result.value)}`;
|
|
@@ -481,6 +905,14 @@ export function formatCursorToolTranscript(toolCall: unknown, options: Transcrip
|
|
|
481
905
|
return formatDelete(args, result, options);
|
|
482
906
|
case "readLints":
|
|
483
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);
|
|
484
916
|
case "mcp":
|
|
485
917
|
return formatMcp(args, result, options);
|
|
486
918
|
default:
|
|
@@ -502,39 +934,96 @@ function buildGenericPiToolDisplay(name: string, args: Record<string, unknown>,
|
|
|
502
934
|
};
|
|
503
935
|
}
|
|
504
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);
|
|
950
|
+
return {
|
|
951
|
+
toolName,
|
|
952
|
+
args,
|
|
953
|
+
result: textToolResult(contentText, {
|
|
954
|
+
...details,
|
|
955
|
+
summary: details.summary ?? summary,
|
|
956
|
+
expandedText: details.expandedText ?? contentText,
|
|
957
|
+
}),
|
|
958
|
+
isError,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
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
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
505
979
|
export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptOptions = {}): CursorPiToolDisplay {
|
|
506
|
-
const
|
|
980
|
+
const rawName = getToolName(toolCall);
|
|
981
|
+
const name = normalizeToolName(rawName);
|
|
507
982
|
const args = getToolArgs(toolCall);
|
|
508
983
|
const result = normalizeResult(getToolResult(toolCall));
|
|
509
984
|
|
|
510
985
|
if (name === "read") {
|
|
511
986
|
const isError = result.status === "error";
|
|
512
|
-
const value = asRecord(result.value);
|
|
513
|
-
const totalLines = getNumber(value, "totalLines");
|
|
514
|
-
const readOptions = {
|
|
515
|
-
...options,
|
|
516
|
-
maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
|
|
517
|
-
maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
|
|
518
|
-
};
|
|
519
987
|
return {
|
|
520
988
|
toolName: "read",
|
|
521
|
-
args,
|
|
522
|
-
result: textToolResult(isError ? formatError(result.error) :
|
|
989
|
+
args: buildReadDisplayArgs(args, options),
|
|
990
|
+
result: textToolResult(isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options)),
|
|
523
991
|
isError,
|
|
524
992
|
};
|
|
525
993
|
}
|
|
526
994
|
|
|
527
995
|
if (name === "shell") {
|
|
528
|
-
const shellOutput = getShellOutput(result);
|
|
529
|
-
const isError = result.status === "error" || (shellOutput.exitCode !== undefined && shellOutput.exitCode !== 0);
|
|
996
|
+
const shellOutput = getShellOutput(result, args);
|
|
997
|
+
const isError = result.status === "error" || shellOutput.timedOut || (shellOutput.exitCode !== undefined && shellOutput.exitCode !== 0);
|
|
530
998
|
return {
|
|
531
999
|
toolName: "bash",
|
|
532
|
-
args,
|
|
1000
|
+
args: buildShellDisplayArgs(args),
|
|
533
1001
|
result: textToolResult(result.status === "error" ? formatError(result.error) : limitText(shellOutput.text, options)),
|
|
534
1002
|
isError,
|
|
535
1003
|
};
|
|
536
1004
|
}
|
|
537
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
|
+
|
|
538
1027
|
if (name === "ls") {
|
|
539
1028
|
return {
|
|
540
1029
|
toolName: "ls",
|
|
@@ -546,35 +1035,215 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
|
|
|
546
1035
|
|
|
547
1036
|
if (name === "edit") {
|
|
548
1037
|
const value = asRecord(result.value);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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"),
|
|
560
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
|
+
);
|
|
561
1074
|
}
|
|
562
1075
|
|
|
563
1076
|
if (name === "write") {
|
|
564
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
|
+
}
|
|
565
1104
|
return {
|
|
566
|
-
toolName: "
|
|
567
|
-
args,
|
|
568
|
-
result: textToolResult(
|
|
569
|
-
cursorToolName: "write",
|
|
570
|
-
path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
|
|
571
|
-
linesCreated: getNumber(value, "linesCreated"),
|
|
572
|
-
fileSize: getNumber(value, "fileSize"),
|
|
573
|
-
}),
|
|
1105
|
+
toolName: "write",
|
|
1106
|
+
args: displayArgs,
|
|
1107
|
+
result: textToolResult(contentText, details),
|
|
574
1108
|
isError: result.status === "error",
|
|
575
1109
|
};
|
|
576
1110
|
}
|
|
577
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
|
+
|
|
578
1247
|
return buildGenericPiToolDisplay(name, args, result, options);
|
|
579
1248
|
}
|
|
580
1249
|
|