pi-cursor-sdk 0.1.9 → 0.1.11

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.
@@ -1,4 +1,4 @@
1
- import { closeSync, openSync, readSync, statSync } from "node:fs";
1
+ import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
 
4
4
  const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
@@ -6,6 +6,8 @@ const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
6
6
  const DEFAULT_MAX_LIST_ITEMS = 200;
7
7
  const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
8
8
  const DEFAULT_READ_TRANSCRIPT_LINES = 12;
9
+ const LOCAL_READ_PREVIEW_NOTICE =
10
+ "[local file preview at transcript time; Cursor read result content was unavailable]";
9
11
 
10
12
  interface TranscriptOptions {
11
13
  maxChars?: number;
@@ -194,15 +196,18 @@ function isSensitivePreviewPath(filePath: string): boolean {
194
196
  function readFilePreview(path: string, options: TranscriptOptions): string | undefined {
195
197
  const cwd = options.cwd ?? process.cwd();
196
198
  const filePath = resolveFilePath(path, cwd);
197
- if (!isPathWithinCwd(filePath, cwd) || isSensitivePreviewPath(filePath)) return undefined;
198
199
 
199
200
  const maxChars = options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS;
200
201
  const maxBytes = Math.max(8192, maxChars * 4);
201
202
  let fd: number | undefined;
202
203
  try {
203
- const stat = statSync(filePath);
204
+ const realCwd = realpathSync(cwd);
205
+ const realFilePath = realpathSync(filePath);
206
+ if (!isPathWithinCwd(realFilePath, realCwd) || isSensitivePreviewPath(filePath) || isSensitivePreviewPath(realFilePath)) return undefined;
207
+
208
+ const stat = statSync(realFilePath);
204
209
  if (!stat.isFile()) return undefined;
205
- fd = openSync(filePath, "r");
210
+ fd = openSync(realFilePath, "r");
206
211
  const buffer = Buffer.alloc(Math.min(stat.size, maxBytes));
207
212
  const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
208
213
  const text = buffer.toString("utf8", 0, bytesRead);
@@ -229,7 +234,10 @@ function getReadContent(args: Record<string, unknown>, result: NormalizedResult,
229
234
  };
230
235
  const value = asRecord(result.value);
231
236
  const resultContent = getString(value, "content");
232
- return resultContent && resultContent.length > 0 ? resultContent : rawPath ? (readFilePreview(rawPath, readOptions) ?? stringifyUnknown(result.value)) : stringifyUnknown(result.value);
237
+ if (resultContent && resultContent.length > 0) return resultContent;
238
+ if (!rawPath) return stringifyUnknown(result.value);
239
+ const localPreview = readFilePreview(rawPath, readOptions);
240
+ return localPreview ? `${LOCAL_READ_PREVIEW_NOTICE}\n${localPreview}` : stringifyUnknown(result.value);
233
241
  }
234
242
 
235
243
  function formatRead(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
@@ -536,6 +544,37 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
536
544
  };
537
545
  }
538
546
 
547
+ if (name === "edit") {
548
+ const value = asRecord(result.value);
549
+ return {
550
+ toolName: "cursor_edit",
551
+ args,
552
+ result: textToolResult(formatEdit(args, result, options), {
553
+ cursorToolName: "edit",
554
+ path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
555
+ linesAdded: getNumber(value, "linesAdded"),
556
+ linesRemoved: getNumber(value, "linesRemoved"),
557
+ diffString: getString(value, "diffString"),
558
+ }),
559
+ isError: result.status === "error",
560
+ };
561
+ }
562
+
563
+ if (name === "write") {
564
+ const value = asRecord(result.value);
565
+ return {
566
+ toolName: "cursor_write",
567
+ args,
568
+ result: textToolResult(formatWrite(args, result, options), {
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
+ }),
574
+ isError: result.status === "error",
575
+ };
576
+ }
577
+
539
578
  return buildGenericPiToolDisplay(name, args, result, options);
540
579
  }
541
580
 
package/src/index.ts CHANGED
@@ -1,9 +1,24 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ProviderConfig, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
2
  import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery.js";
3
3
  import { registerCursorFastControls } from "./cursor-state.js";
4
4
  import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display.js";
5
5
  import { streamCursor } from "./cursor-provider.js";
6
6
 
7
+ function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
8
+ return {
9
+ name: "Cursor",
10
+ baseUrl: "https://cursor.com",
11
+ apiKey: "CURSOR_API_KEY",
12
+ api: "cursor-sdk",
13
+ models,
14
+ streamSimple: streamCursor,
15
+ };
16
+ }
17
+
18
+ function registerCursorProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void {
19
+ pi.registerProvider("cursor", createCursorProviderConfig(models));
20
+ }
21
+
7
22
  export default async function (pi: ExtensionAPI) {
8
23
  registerCursorFastControls(pi);
9
24
  registerCursorNativeToolDisplay(pi);
@@ -21,12 +36,24 @@ export default async function (pi: ExtensionAPI) {
21
36
  });
22
37
  }
23
38
 
24
- pi.registerProvider("cursor", {
25
- name: "Cursor",
26
- baseUrl: "https://cursor.com",
27
- apiKey: "CURSOR_API_KEY",
28
- api: "cursor-sdk",
29
- models,
30
- streamSimple: streamCursor,
39
+ pi.registerCommand("cursor-refresh-models", {
40
+ description: "Refresh the live Cursor model catalog without restarting pi",
41
+ handler: async (_args, ctx) => {
42
+ let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
43
+ const refreshedModels = await discoverModels({
44
+ onFallback: (issue) => {
45
+ refreshFallbackIssue = issue;
46
+ },
47
+ });
48
+ registerCursorProvider(pi, refreshedModels);
49
+ if (!ctx.hasUI) return;
50
+ if (refreshFallbackIssue) {
51
+ ctx.ui.notify(`Cursor model catalog refresh still using fallback models: ${refreshFallbackIssue.message}`, "warning");
52
+ } else {
53
+ ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
54
+ }
55
+ },
31
56
  });
57
+
58
+ registerCursorProvider(pi, models);
32
59
  }
@@ -8,6 +8,7 @@ import type {
8
8
  import { AuthStorage, type ProviderModelConfig } from "@earendil-works/pi-coding-agent";
9
9
  import type { ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
10
10
  import { loadContextWindowCache } from "./context-window-cache.js";
11
+ import { FALLBACK_MODEL_ITEMS } from "./cursor-fallback-models.generated.js";
11
12
 
12
13
  const CURSOR_PROVIDER_ID = "cursor";
13
14
  const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
@@ -17,148 +18,7 @@ const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
17
18
  const TEXT_AND_IMAGE_INPUT: ProviderModelConfig["input"] = ["text", "image"];
18
19
  const AUTH_SETUP_HINT = "/login (Use an API key -> Cursor), CURSOR_API_KEY, or --api-key";
19
20
  const CATALOG_REFRESH_HINT =
20
- "After adding auth to an already-started pi session, run /reload or restart pi to refresh the full live Cursor model catalog.";
21
-
22
- const FALLBACK_MODEL_ITEMS: ModelListItem[] = [
23
- {
24
- id: "composer-2",
25
- displayName: "Cursor Composer 2",
26
- parameters: [
27
- {
28
- id: "fast",
29
- displayName: "Fast",
30
- values: [{ value: "false" }, { value: "true" }],
31
- },
32
- ],
33
- variants: [
34
- {
35
- params: [{ id: "fast", value: "true" }],
36
- displayName: "Cursor Composer 2",
37
- isDefault: true,
38
- },
39
- ],
40
- },
41
- {
42
- id: "gpt-5.5",
43
- displayName: "GPT-5.5",
44
- parameters: [
45
- {
46
- id: "context",
47
- displayName: "Context",
48
- values: [{ value: "1m" }, { value: "272k" }],
49
- },
50
- {
51
- id: "reasoning",
52
- displayName: "Reasoning",
53
- values: [
54
- { value: "none" },
55
- { value: "low" },
56
- { value: "medium" },
57
- { value: "high" },
58
- { value: "extra-high" },
59
- ],
60
- },
61
- {
62
- id: "fast",
63
- displayName: "Fast",
64
- values: [{ value: "false" }, { value: "true" }],
65
- },
66
- ],
67
- variants: [
68
- {
69
- params: [
70
- { id: "context", value: "1m" },
71
- { id: "reasoning", value: "medium" },
72
- { id: "fast", value: "false" },
73
- ],
74
- displayName: "GPT-5.5",
75
- isDefault: true,
76
- },
77
- ],
78
- },
79
- {
80
- id: "claude-sonnet-4-6",
81
- displayName: "Sonnet 4.6",
82
- parameters: [
83
- {
84
- id: "thinking",
85
- displayName: "Thinking",
86
- values: [{ value: "false" }, { value: "true" }],
87
- },
88
- {
89
- id: "context",
90
- displayName: "Context",
91
- values: [{ value: "1m" }, { value: "200k" }],
92
- },
93
- {
94
- id: "effort",
95
- displayName: "Effort",
96
- values: [
97
- { value: "low" },
98
- { value: "medium" },
99
- { value: "high" },
100
- { value: "xhigh" },
101
- { value: "max" },
102
- ],
103
- },
104
- {
105
- id: "fast",
106
- displayName: "Fast",
107
- values: [{ value: "false" }, { value: "true" }],
108
- },
109
- ],
110
- variants: [
111
- {
112
- params: [
113
- { id: "thinking", value: "true" },
114
- { id: "context", value: "1m" },
115
- { id: "effort", value: "medium" },
116
- { id: "fast", value: "false" },
117
- ],
118
- displayName: "Sonnet 4.6",
119
- isDefault: true,
120
- },
121
- ],
122
- },
123
- {
124
- id: "claude-opus-4-7",
125
- displayName: "Opus 4.7",
126
- parameters: [
127
- {
128
- id: "thinking",
129
- displayName: "Thinking",
130
- values: [{ value: "false" }, { value: "true" }],
131
- },
132
- {
133
- id: "context",
134
- displayName: "Context",
135
- values: [{ value: "1m" }, { value: "300k" }],
136
- },
137
- {
138
- id: "effort",
139
- displayName: "Effort",
140
- values: [
141
- { value: "low" },
142
- { value: "medium" },
143
- { value: "high" },
144
- { value: "xhigh" },
145
- { value: "max" },
146
- ],
147
- },
148
- ],
149
- variants: [
150
- {
151
- params: [
152
- { id: "thinking", value: "true" },
153
- { id: "context", value: "1m" },
154
- { id: "effort", value: "xhigh" },
155
- ],
156
- displayName: "Opus 4.7",
157
- isDefault: true,
158
- },
159
- ],
160
- },
161
- ];
21
+ "After adding auth to an already-started pi session, run /cursor-refresh-models to refresh the full live Cursor model catalog without restarting pi.";
162
22
 
163
23
  export type CursorModelFallbackReason = "missing-api-key" | "discovery-failed" | "empty-model-list";
164
24