pi-cursor-sdk 0.1.16 → 0.1.18
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 +53 -1
- package/README.md +2 -2
- package/docs/cursor-live-smoke-checklist.md +54 -41
- package/docs/cursor-model-ux-spec.md +4 -3
- package/docs/cursor-testing-lessons.md +199 -0
- package/package.json +14 -5
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/steering-rpc-smoke.mjs +238 -0
- package/scripts/tmux-live-smoke.sh +418 -0
- package/scripts/validate-smoke-jsonl.mjs +207 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-edit-diff.ts +11 -0
- package/src/cursor-env-boolean.ts +22 -0
- package/src/cursor-live-run-coordinator.ts +483 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +103 -0
- package/src/cursor-native-tool-display-replay.ts +465 -0
- package/src/cursor-native-tool-display-state.ts +78 -0
- package/src/cursor-native-tool-display-tools.ts +102 -0
- package/src/cursor-native-tool-display.ts +10 -648
- package/src/cursor-partial-content-emitter.ts +121 -0
- package/src/cursor-pi-tool-bridge-abort.ts +133 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
- package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
- package/src/cursor-pi-tool-bridge-run.ts +384 -0
- package/src/cursor-pi-tool-bridge-server.ts +182 -0
- package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
- package/src/cursor-pi-tool-bridge-types.ts +80 -0
- package/src/cursor-pi-tool-bridge.ts +42 -1104
- package/src/cursor-provider-live-run-drain.ts +405 -0
- package/src/cursor-provider-turn-coordinator.ts +460 -0
- package/src/cursor-provider.ts +77 -1103
- package/src/cursor-question-tool.ts +9 -1
- package/src/cursor-record-utils.ts +26 -0
- package/src/cursor-sdk-output-filter.ts +100 -0
- package/src/cursor-sensitive-text.ts +37 -0
- package/src/cursor-tool-transcript.ts +28 -1229
- package/src/cursor-transcript-tool-formatters.ts +641 -0
- package/src/cursor-transcript-tool-specs.ts +441 -0
- package/src/cursor-transcript-utils.ts +276 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
|
|
4
|
+
|
|
5
|
+
export { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
|
|
6
|
+
|
|
7
|
+
export interface TranscriptOptions {
|
|
8
|
+
maxChars?: number;
|
|
9
|
+
maxLines?: number;
|
|
10
|
+
maxListItems?: number;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NormalizedResult {
|
|
15
|
+
status: string | undefined;
|
|
16
|
+
value: unknown;
|
|
17
|
+
error: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PiToolDisplayContent {
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PiToolDisplayResult {
|
|
26
|
+
content: PiToolDisplayContent[];
|
|
27
|
+
details?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CursorPiToolDisplay {
|
|
31
|
+
toolName: string;
|
|
32
|
+
args: Record<string, unknown>;
|
|
33
|
+
result: PiToolDisplayResult;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
|
|
38
|
+
export const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
|
|
39
|
+
export const DEFAULT_MAX_LIST_ITEMS = 200;
|
|
40
|
+
export const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
|
|
41
|
+
export const DEFAULT_READ_TRANSCRIPT_LINES = 12;
|
|
42
|
+
export const DEFAULT_NATIVE_READ_DISPLAY_LINES = 20;
|
|
43
|
+
export const LOCAL_READ_PREVIEW_NOTICE =
|
|
44
|
+
"[local file preview at transcript time; Cursor read result content was unavailable]";
|
|
45
|
+
|
|
46
|
+
export function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
47
|
+
const value = record?.[key];
|
|
48
|
+
return typeof value === "string" ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
52
|
+
const value = record?.[key];
|
|
53
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
57
|
+
const value = record?.[key];
|
|
58
|
+
return typeof value === "boolean" ? value : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
|
|
62
|
+
return asRecord(record?.[key]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getArray(record: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
|
|
66
|
+
const value = record?.[key];
|
|
67
|
+
return Array.isArray(value) ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getToolName(toolCall: unknown): string {
|
|
71
|
+
const record = asRecord(toolCall);
|
|
72
|
+
return getString(record, "name") ?? getString(record, "type") ?? getString(record, "toolName") ?? "unknown";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getToolArgs(toolCall: unknown): Record<string, unknown> {
|
|
76
|
+
const record = asRecord(toolCall);
|
|
77
|
+
return getRecord(record, "args") ?? getRecord(record, "input") ?? {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getToolResult(toolCall: unknown): unknown {
|
|
81
|
+
const record = asRecord(toolCall);
|
|
82
|
+
return record?.result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeToolName(name: string): string {
|
|
86
|
+
const normalized = name.replace(/\s+/g, " ").trim();
|
|
87
|
+
const normalizedKey = normalized.toLowerCase();
|
|
88
|
+
switch (normalizedKey) {
|
|
89
|
+
case "read_file":
|
|
90
|
+
return "read";
|
|
91
|
+
case "list_dir":
|
|
92
|
+
return "ls";
|
|
93
|
+
case "run_terminal_cmd":
|
|
94
|
+
case "terminal":
|
|
95
|
+
case "bash":
|
|
96
|
+
case "shell":
|
|
97
|
+
return "shell";
|
|
98
|
+
case "grep_search":
|
|
99
|
+
case "search":
|
|
100
|
+
return "grep";
|
|
101
|
+
case "file_search":
|
|
102
|
+
return "glob";
|
|
103
|
+
case "write_file":
|
|
104
|
+
case "writefile":
|
|
105
|
+
return "write";
|
|
106
|
+
case "strreplace":
|
|
107
|
+
case "str_replace":
|
|
108
|
+
case "str-replace":
|
|
109
|
+
case "edit_file":
|
|
110
|
+
case "editfile":
|
|
111
|
+
case "edit_notebook":
|
|
112
|
+
case "editnotebook":
|
|
113
|
+
case "notebook_edit":
|
|
114
|
+
case "notebookedit":
|
|
115
|
+
return "edit";
|
|
116
|
+
default:
|
|
117
|
+
return normalized || "unknown";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function normalizeResult(result: unknown): NormalizedResult {
|
|
122
|
+
const record = asRecord(result);
|
|
123
|
+
const status = getString(record, "status");
|
|
124
|
+
if (status === "success" || status === "error") {
|
|
125
|
+
return { status, value: record?.value, error: record?.error };
|
|
126
|
+
}
|
|
127
|
+
return { status, value: result, error: undefined };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function stringifyUnknown(value: unknown): string {
|
|
131
|
+
if (value === undefined) return "";
|
|
132
|
+
if (typeof value === "string") return value;
|
|
133
|
+
try {
|
|
134
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
135
|
+
} catch {
|
|
136
|
+
return String(value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function limitText(text: string, options: TranscriptOptions = {}, knownTotalLines?: number): string {
|
|
141
|
+
const maxChars = options.maxChars ?? DEFAULT_MAX_TRANSCRIPT_CHARS;
|
|
142
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_TRANSCRIPT_LINES;
|
|
143
|
+
const lines = text.split("\n");
|
|
144
|
+
let limitedLines = lines.slice(0, maxLines);
|
|
145
|
+
let limited = limitedLines.join("\n");
|
|
146
|
+
let truncatedLines = Math.max((knownTotalLines ?? lines.length) - limitedLines.length, 0);
|
|
147
|
+
let truncatedChars = 0;
|
|
148
|
+
|
|
149
|
+
if (limited.length > maxChars) {
|
|
150
|
+
truncatedChars += limited.length - maxChars;
|
|
151
|
+
limited = limited.slice(0, maxChars);
|
|
152
|
+
limitedLines = limited.split("\n");
|
|
153
|
+
truncatedLines = Math.max(truncatedLines, Math.max((knownTotalLines ?? lines.length) - limitedLines.length, 0));
|
|
154
|
+
}
|
|
155
|
+
if (text.length > limited.length) {
|
|
156
|
+
truncatedChars += Math.max(text.length - limited.length - truncatedChars, 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const suffixParts: string[] = [];
|
|
160
|
+
if (truncatedLines > 0) suffixParts.push(`${truncatedLines} more lines`);
|
|
161
|
+
if (truncatedChars > 0 && truncatedLines === 0) suffixParts.push(`${truncatedChars} more chars`);
|
|
162
|
+
return suffixParts.length > 0 ? `${limited}\n... (${suffixParts.join(", ")} truncated)` : limited;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function limitItems<T>(items: T[], options: TranscriptOptions = {}): { items: T[]; omitted: number } {
|
|
166
|
+
const maxListItems = options.maxListItems ?? DEFAULT_MAX_LIST_ITEMS;
|
|
167
|
+
return { items: items.slice(0, maxListItems), omitted: Math.max(items.length - maxListItems, 0) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function joinSections(header: string, body?: string): string {
|
|
171
|
+
const trimmedBody = body?.trimEnd();
|
|
172
|
+
return trimmedBody ? `${header}\n\n${trimmedBody}\n` : `${header}\n`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function formatError(error: unknown): string {
|
|
176
|
+
const text = stringifyUnknown(error).trim();
|
|
177
|
+
return text ? `Error: ${text}` : "Error";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function formatDisplayPath(path: string, cwd = process.cwd()): string {
|
|
181
|
+
const trimmed = path.trim();
|
|
182
|
+
if (!trimmed) return trimmed;
|
|
183
|
+
if (!isAbsolute(trimmed)) return trimmed;
|
|
184
|
+
const relativePath = relative(cwd, trimmed);
|
|
185
|
+
if (!relativePath || relativePath === "") return ".";
|
|
186
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return trimmed;
|
|
187
|
+
return relativePath;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatDiffPath(path: string, cwd = process.cwd()): string {
|
|
191
|
+
if (path === "/dev/null") return path;
|
|
192
|
+
return formatDisplayPath(path, cwd);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatDiffHeaderLine(line: string, options: TranscriptOptions): string {
|
|
196
|
+
const match = /^(---|\+\+\+)\s+((?:[ab]\/)?)(.+)$/.exec(line);
|
|
197
|
+
if (!match) return line;
|
|
198
|
+
const [, marker, prefix, rawPath] = match;
|
|
199
|
+
if (!prefix && rawPath !== "/dev/null") return line;
|
|
200
|
+
const displayPath = formatDiffPath(rawPath, options.cwd);
|
|
201
|
+
return `${marker} ${prefix}${displayPath}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function formatDiffString(diff: string | undefined, options: TranscriptOptions): string | undefined {
|
|
205
|
+
return diff
|
|
206
|
+
?.split("\n")
|
|
207
|
+
.map((line) => formatDiffHeaderLine(line, options))
|
|
208
|
+
.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveFilePath(path: string, cwd = process.cwd()): string {
|
|
212
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isPathWithinCwd(filePath: string, cwd = process.cwd()): boolean {
|
|
216
|
+
const relativePath = relative(cwd, filePath);
|
|
217
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function isSensitivePreviewPath(filePath: string): boolean {
|
|
221
|
+
const segments = filePath.split(/[\\/]+/).map((segment) => segment.toLowerCase());
|
|
222
|
+
const basename = segments.at(-1) ?? "";
|
|
223
|
+
return (
|
|
224
|
+
segments.includes(".ssh") ||
|
|
225
|
+
segments.includes("secrets") ||
|
|
226
|
+
basename === ".env" ||
|
|
227
|
+
basename.startsWith(".env.") ||
|
|
228
|
+
basename === ".npmrc" ||
|
|
229
|
+
basename === ".netrc" ||
|
|
230
|
+
basename === "credentials" ||
|
|
231
|
+
basename === "id_rsa" ||
|
|
232
|
+
basename === "id_ed25519" ||
|
|
233
|
+
/\.(?:pem|key|p12|pfx)$/i.test(basename)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function readFilePreview(path: string, options: TranscriptOptions): string | undefined {
|
|
238
|
+
const cwd = options.cwd ?? process.cwd();
|
|
239
|
+
const filePath = resolveFilePath(path, cwd);
|
|
240
|
+
|
|
241
|
+
const maxChars = options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS;
|
|
242
|
+
const maxBytes = Math.max(8192, maxChars * 4);
|
|
243
|
+
let fd: number | undefined;
|
|
244
|
+
try {
|
|
245
|
+
const realCwd = realpathSync(cwd);
|
|
246
|
+
const realFilePath = realpathSync(filePath);
|
|
247
|
+
if (!isPathWithinCwd(realFilePath, realCwd) || isSensitivePreviewPath(filePath) || isSensitivePreviewPath(realFilePath)) return undefined;
|
|
248
|
+
|
|
249
|
+
const stat = statSync(realFilePath);
|
|
250
|
+
if (!stat.isFile()) return undefined;
|
|
251
|
+
fd = openSync(realFilePath, "r");
|
|
252
|
+
const buffer = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
253
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
254
|
+
const text = buffer.toString("utf8", 0, bytesRead);
|
|
255
|
+
if (text.includes("\0")) return undefined;
|
|
256
|
+
return text;
|
|
257
|
+
} catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
} finally {
|
|
260
|
+
if (fd !== undefined) closeSync(fd);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function formatPathArg(args: Record<string, unknown>, options: TranscriptOptions, key = "path"): string | undefined {
|
|
265
|
+
const path = args[key];
|
|
266
|
+
return typeof path === "string" && path.trim() ? formatDisplayPath(path, options.cwd) : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
export function firstNonEmptyLine(text: string): string | undefined {
|
|
271
|
+
return text.split("\n").find((line) => line.trim())?.trim();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function truncateArg(value: string, maxLength = 120): string {
|
|
275
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
|
276
|
+
}
|