pi-cursor-sdk 0.0.0 → 0.1.1

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.
@@ -0,0 +1,189 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
5
+ import { getCursorModelMetadata } from "./model-discovery.js";
6
+
7
+ const CURSOR_PROVIDER = "cursor";
8
+ const FAST_ENTRY_TYPE = "cursor-fast-state";
9
+ const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
10
+
11
+ interface CursorFastEntryData {
12
+ baseModelId: string;
13
+ fast: boolean;
14
+ }
15
+
16
+ interface CursorGlobalConfig {
17
+ fastDefaults?: Record<string, boolean>;
18
+ }
19
+
20
+ const sessionFastPreferences = new Map<string, boolean>();
21
+ let globalFastPreferences = new Map<string, boolean>();
22
+ let cliForceFast = false;
23
+ let cliForceNoFast = false;
24
+
25
+ function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
26
+ if (!value || typeof value !== "object") return false;
27
+ const data = value as Record<string, unknown>;
28
+ return typeof data.baseModelId === "string" && typeof data.fast === "boolean";
29
+ }
30
+
31
+ function getConfigPath(): string {
32
+ return join(getAgentDir(), GLOBAL_CONFIG_FILE);
33
+ }
34
+
35
+ function loadGlobalFastPreferences(): Map<string, boolean> {
36
+ const path = getConfigPath();
37
+ if (!existsSync(path)) return new Map();
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as CursorGlobalConfig;
40
+ return new Map(
41
+ Object.entries(parsed.fastDefaults ?? {}).filter(
42
+ (entry): entry is [string, boolean] => typeof entry[1] === "boolean",
43
+ ),
44
+ );
45
+ } catch {
46
+ return new Map();
47
+ }
48
+ }
49
+
50
+ function saveGlobalFastPreferences(): void {
51
+ const path = getConfigPath();
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ const config: CursorGlobalConfig = {
54
+ fastDefaults: Object.fromEntries([...globalFastPreferences.entries()].sort(([a], [b]) => a.localeCompare(b))),
55
+ };
56
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
57
+ }
58
+
59
+ function restoreSessionFastPreferences(ctx: ExtensionContext): void {
60
+ sessionFastPreferences.clear();
61
+ for (const entry of ctx.sessionManager.getBranch()) {
62
+ if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
63
+ if (isCursorFastEntryData(entry.data)) {
64
+ sessionFastPreferences.set(entry.data.baseModelId, entry.data.fast);
65
+ }
66
+ }
67
+ }
68
+
69
+ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undefined {
70
+ const metadata = getCursorModelMetadata(modelId);
71
+ if (!metadata?.supportsFast) return undefined;
72
+ if (cliForceNoFast) return false;
73
+ if (cliForceFast) return true;
74
+ return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
75
+ }
76
+
77
+ function updateCursorStatus(ctx: ExtensionContext, model = ctx.model): void {
78
+ if (model?.provider !== CURSOR_PROVIDER) {
79
+ ctx.ui.setStatus("cursor", undefined);
80
+ return;
81
+ }
82
+ const metadata = getCursorModelMetadata(model.id);
83
+ if (!metadata) {
84
+ ctx.ui.setStatus("cursor", undefined);
85
+ return;
86
+ }
87
+ const fast = getEffectiveFast(metadata.baseModelId, model.id);
88
+ ctx.ui.setStatus("cursor", fast ? "cursor fast" : undefined);
89
+ }
90
+
91
+ function getCurrentCursorMetadata(ctx: ExtensionContext) {
92
+ const model = ctx.model;
93
+ if (model?.provider !== CURSOR_PROVIDER) return undefined;
94
+ return getCursorModelMetadata(model.id);
95
+ }
96
+
97
+ function restoreMapValue(map: Map<string, boolean>, key: string, previous: boolean | undefined): void {
98
+ if (previous === undefined) {
99
+ map.delete(key);
100
+ } else {
101
+ map.set(key, previous);
102
+ }
103
+ }
104
+
105
+ function persistFastPreference(pi: ExtensionAPI, baseModelId: string, fast: boolean): void {
106
+ const previousSession = sessionFastPreferences.get(baseModelId);
107
+ const previousGlobal = globalFastPreferences.get(baseModelId);
108
+ sessionFastPreferences.set(baseModelId, fast);
109
+ globalFastPreferences.set(baseModelId, fast);
110
+ try {
111
+ saveGlobalFastPreferences();
112
+ } catch (error) {
113
+ restoreMapValue(sessionFastPreferences, baseModelId, previousSession);
114
+ restoreMapValue(globalFastPreferences, baseModelId, previousGlobal);
115
+ throw error;
116
+ }
117
+ pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { baseModelId, fast });
118
+ }
119
+
120
+ export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
121
+ const metadata = getCursorModelMetadata(modelId);
122
+ if (!metadata) return undefined;
123
+ return getEffectiveFast(metadata.baseModelId, modelId);
124
+ }
125
+
126
+ export function registerCursorFastControls(pi: ExtensionAPI): void {
127
+ pi.registerFlag("cursor-fast", {
128
+ description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
129
+ type: "boolean",
130
+ default: false,
131
+ });
132
+
133
+ pi.registerFlag("cursor-no-fast", {
134
+ description: "Force Cursor fast mode off for this run when the selected Cursor model supports it",
135
+ type: "boolean",
136
+ default: false,
137
+ });
138
+
139
+ pi.registerCommand("cursor-fast", {
140
+ description: "Toggle Cursor fast mode for the selected Cursor model",
141
+ handler: async (_args, ctx) => {
142
+ const metadata = getCurrentCursorMetadata(ctx);
143
+ if (!metadata?.supportsFast || !ctx.model) {
144
+ const modelName = ctx.model?.id ?? "current model";
145
+ ctx.ui.notify(`Fast mode not supported by ${modelName}`, "info");
146
+ return;
147
+ }
148
+ if (cliForceNoFast) {
149
+ ctx.ui.notify("Cursor fast is forced off by --cursor-no-fast", "info");
150
+ return;
151
+ }
152
+ if (cliForceFast) {
153
+ ctx.ui.notify("Cursor fast is forced by --cursor-fast", "info");
154
+ return;
155
+ }
156
+
157
+ const current = getEffectiveFast(metadata.baseModelId, metadata.piModelId) ?? false;
158
+ const next = !current;
159
+ try {
160
+ persistFastPreference(pi, metadata.baseModelId, next);
161
+ } catch (error) {
162
+ updateCursorStatus(ctx);
163
+ ctx.ui.notify(`Failed to save Cursor fast preference: ${error instanceof Error ? error.message : String(error)}`, "error");
164
+ return;
165
+ }
166
+ updateCursorStatus(ctx);
167
+ ctx.ui.notify(`Cursor fast ${next ? "enabled" : "disabled"}`, "info");
168
+ },
169
+ });
170
+
171
+ pi.on("session_start", async (_event, ctx) => {
172
+ globalFastPreferences = loadGlobalFastPreferences();
173
+ cliForceFast = pi.getFlag("cursor-fast") === true;
174
+ cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
175
+ restoreSessionFastPreferences(ctx);
176
+ updateCursorStatus(ctx);
177
+ });
178
+
179
+ pi.on("model_select", async (event, ctx) => {
180
+ updateCursorStatus(ctx, event.model);
181
+ });
182
+ }
183
+
184
+ export const __testUtils = {
185
+ FAST_ENTRY_TYPE,
186
+ getConfigPath,
187
+ loadGlobalFastPreferences,
188
+ sessionFastPreferences,
189
+ };
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery.js";
3
+ import { registerCursorFastControls } from "./cursor-state.js";
4
+ import { streamCursor } from "./cursor-provider.js";
5
+
6
+ export default async function (pi: ExtensionAPI) {
7
+ registerCursorFastControls(pi);
8
+ let fallbackIssue: CursorModelFallbackIssue | undefined;
9
+ const models = await discoverModels({
10
+ onFallback: (issue) => {
11
+ fallbackIssue = issue;
12
+ },
13
+ });
14
+
15
+ if (fallbackIssue) {
16
+ const issue = fallbackIssue;
17
+ pi.on("session_start", async (_event, ctx) => {
18
+ if (ctx.hasUI) ctx.ui.notify(issue.message, "warning");
19
+ });
20
+ }
21
+
22
+ pi.registerProvider("cursor", {
23
+ name: "Cursor",
24
+ baseUrl: "https://cursor.com",
25
+ apiKey: "CURSOR_API_KEY",
26
+ api: "cursor-sdk",
27
+ models,
28
+ streamSimple: streamCursor,
29
+ });
30
+ }