@oh-my-pi/pi-ai 4.2.2 → 4.3.0
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/package.json +5 -1
- package/src/index.ts +1 -0
- package/src/models.generated.ts +224 -1
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +1998 -0
- package/src/stream.ts +16 -0
- package/src/types.ts +55 -1
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/index.ts +17 -0
- package/src/utils/oauth/types.ts +2 -1
package/src/stream.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { supportsXhigh } from "./models";
|
|
5
5
|
import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic";
|
|
6
|
+
import { type CursorOptions, streamCursor } from "./providers/cursor";
|
|
6
7
|
import { type GoogleOptions, streamGoogle } from "./providers/google";
|
|
7
8
|
import {
|
|
8
9
|
type GoogleGeminiCliOptions,
|
|
@@ -82,6 +83,7 @@ export function getEnvApiKey(provider: any): string | undefined {
|
|
|
82
83
|
zai: "ZAI_API_KEY",
|
|
83
84
|
mistral: "MISTRAL_API_KEY",
|
|
84
85
|
opencode: "OPENCODE_API_KEY",
|
|
86
|
+
cursor: "CURSOR_ACCESS_TOKEN",
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
const envVar = envMap[provider];
|
|
@@ -128,6 +130,9 @@ export function stream<TApi extends Api>(
|
|
|
128
130
|
providerOptions as GoogleGeminiCliOptions,
|
|
129
131
|
);
|
|
130
132
|
|
|
133
|
+
case "cursor-agent":
|
|
134
|
+
return streamCursor(model as Model<"cursor-agent">, context, providerOptions as CursorOptions);
|
|
135
|
+
|
|
131
136
|
default: {
|
|
132
137
|
// This should never be reached if all Api cases are handled
|
|
133
138
|
const _exhaustive: never = api;
|
|
@@ -185,6 +190,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
185
190
|
signal: options?.signal,
|
|
186
191
|
apiKey: apiKey || options?.apiKey,
|
|
187
192
|
sessionId: options?.sessionId,
|
|
193
|
+
execHandlers: options?.execHandlers,
|
|
188
194
|
};
|
|
189
195
|
|
|
190
196
|
// Helper to clamp xhigh to high for providers that don't support it
|
|
@@ -353,6 +359,16 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
353
359
|
} satisfies GoogleVertexOptions;
|
|
354
360
|
}
|
|
355
361
|
|
|
362
|
+
case "cursor-agent": {
|
|
363
|
+
const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
|
|
364
|
+
const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
|
|
365
|
+
return {
|
|
366
|
+
...base,
|
|
367
|
+
execHandlers,
|
|
368
|
+
onToolResult,
|
|
369
|
+
} satisfies CursorOptions;
|
|
370
|
+
}
|
|
371
|
+
|
|
356
372
|
default: {
|
|
357
373
|
// Exhaustiveness check
|
|
358
374
|
const _exhaustive: never = model.api;
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
2
|
+
import type { CursorOptions } from "./providers/cursor";
|
|
3
|
+
import type {
|
|
4
|
+
DeleteArgs,
|
|
5
|
+
DeleteResult,
|
|
6
|
+
DiagnosticsArgs,
|
|
7
|
+
DiagnosticsResult,
|
|
8
|
+
GrepArgs,
|
|
9
|
+
GrepResult,
|
|
10
|
+
LsArgs,
|
|
11
|
+
LsResult,
|
|
12
|
+
McpResult,
|
|
13
|
+
ReadArgs,
|
|
14
|
+
ReadResult,
|
|
15
|
+
ShellArgs,
|
|
16
|
+
ShellResult,
|
|
17
|
+
WriteArgs,
|
|
18
|
+
WriteResult,
|
|
19
|
+
} from "./providers/cursor/gen/agent_pb";
|
|
2
20
|
import type { GoogleOptions } from "./providers/google";
|
|
3
21
|
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
|
|
4
22
|
import type { GoogleVertexOptions } from "./providers/google-vertex";
|
|
@@ -16,7 +34,8 @@ export type Api =
|
|
|
16
34
|
| "anthropic-messages"
|
|
17
35
|
| "google-generative-ai"
|
|
18
36
|
| "google-gemini-cli"
|
|
19
|
-
| "google-vertex"
|
|
37
|
+
| "google-vertex"
|
|
38
|
+
| "cursor-agent";
|
|
20
39
|
|
|
21
40
|
export interface ApiOptionsMap {
|
|
22
41
|
"anthropic-messages": AnthropicOptions;
|
|
@@ -26,6 +45,7 @@ export interface ApiOptionsMap {
|
|
|
26
45
|
"google-generative-ai": GoogleOptions;
|
|
27
46
|
"google-gemini-cli": GoogleGeminiCliOptions;
|
|
28
47
|
"google-vertex": GoogleVertexOptions;
|
|
48
|
+
"cursor-agent": CursorOptions;
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
// Compile-time exhaustiveness check - this will fail if ApiOptionsMap doesn't have all KnownApi keys
|
|
@@ -49,6 +69,7 @@ export type KnownProvider =
|
|
|
49
69
|
| "openai"
|
|
50
70
|
| "openai-codex"
|
|
51
71
|
| "github-copilot"
|
|
72
|
+
| "cursor"
|
|
52
73
|
| "xai"
|
|
53
74
|
| "groq"
|
|
54
75
|
| "cerebras"
|
|
@@ -80,6 +101,8 @@ export interface StreamOptions {
|
|
|
80
101
|
* session-aware features. Ignored by providers that don't support it.
|
|
81
102
|
*/
|
|
82
103
|
sessionId?: string;
|
|
104
|
+
/** Cursor exec/MCP tool handlers (cursor-agent only). */
|
|
105
|
+
execHandlers?: CursorExecHandlers;
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
|
@@ -87,6 +110,10 @@ export interface SimpleStreamOptions extends StreamOptions {
|
|
|
87
110
|
reasoning?: ThinkingLevel;
|
|
88
111
|
/** Custom token budgets for thinking levels (token-based providers only) */
|
|
89
112
|
thinkingBudgets?: ThinkingBudgets;
|
|
113
|
+
/** Cursor exec handlers for local tool execution */
|
|
114
|
+
cursorExecHandlers?: CursorExecHandlers;
|
|
115
|
+
/** Hook to handle tool results from Cursor exec */
|
|
116
|
+
cursorOnToolResult?: CursorToolResultHandler;
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
// Generic StreamFunction with typed options
|
|
@@ -169,6 +196,33 @@ export interface ToolResultMessage<TDetails = any> {
|
|
|
169
196
|
|
|
170
197
|
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
|
171
198
|
|
|
199
|
+
export type CursorExecHandlerResult<T> = { result: T; toolResult?: ToolResultMessage } | T | ToolResultMessage;
|
|
200
|
+
|
|
201
|
+
export type CursorToolResultHandler = (
|
|
202
|
+
result: ToolResultMessage,
|
|
203
|
+
) => ToolResultMessage | undefined | Promise<ToolResultMessage | undefined>;
|
|
204
|
+
|
|
205
|
+
export interface CursorMcpCall {
|
|
206
|
+
name: string;
|
|
207
|
+
providerIdentifier: string;
|
|
208
|
+
toolName: string;
|
|
209
|
+
toolCallId: string;
|
|
210
|
+
args: Record<string, unknown>;
|
|
211
|
+
rawArgs: Record<string, Uint8Array>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface CursorExecHandlers {
|
|
215
|
+
read?: (args: ReadArgs) => Promise<CursorExecHandlerResult<ReadResult>>;
|
|
216
|
+
ls?: (args: LsArgs) => Promise<CursorExecHandlerResult<LsResult>>;
|
|
217
|
+
grep?: (args: GrepArgs) => Promise<CursorExecHandlerResult<GrepResult>>;
|
|
218
|
+
write?: (args: WriteArgs) => Promise<CursorExecHandlerResult<WriteResult>>;
|
|
219
|
+
delete?: (args: DeleteArgs) => Promise<CursorExecHandlerResult<DeleteResult>>;
|
|
220
|
+
shell?: (args: ShellArgs) => Promise<CursorExecHandlerResult<ShellResult>>;
|
|
221
|
+
diagnostics?: (args: DiagnosticsArgs) => Promise<CursorExecHandlerResult<DiagnosticsResult>>;
|
|
222
|
+
mcp?: (call: CursorMcpCall) => Promise<CursorExecHandlerResult<McpResult>>;
|
|
223
|
+
onToolResult?: CursorToolResultHandler;
|
|
224
|
+
}
|
|
225
|
+
|
|
172
226
|
import type { TSchema } from "@sinclair/typebox";
|
|
173
227
|
|
|
174
228
|
export interface Tool<TParameters extends TSchema = TSchema> {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { generatePKCE } from "./pkce";
|
|
2
|
+
import type { OAuthCredentials } from "./types";
|
|
3
|
+
|
|
4
|
+
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
5
|
+
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
6
|
+
const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
|
|
7
|
+
|
|
8
|
+
const POLL_MAX_ATTEMPTS = 150;
|
|
9
|
+
const POLL_BASE_DELAY = 1000;
|
|
10
|
+
const POLL_MAX_DELAY = 10000;
|
|
11
|
+
const POLL_BACKOFF_MULTIPLIER = 1.2;
|
|
12
|
+
|
|
13
|
+
function sleep(ms: number): Promise<void> {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CursorAuthParams {
|
|
18
|
+
verifier: string;
|
|
19
|
+
challenge: string;
|
|
20
|
+
uuid: string;
|
|
21
|
+
loginUrl: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
|
|
25
|
+
const { verifier, challenge } = await generatePKCE();
|
|
26
|
+
const uuid = crypto.randomUUID();
|
|
27
|
+
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
challenge,
|
|
30
|
+
uuid,
|
|
31
|
+
mode: "login",
|
|
32
|
+
redirectTarget: "cli",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
|
|
36
|
+
|
|
37
|
+
return { verifier, challenge, uuid, loginUrl };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function pollCursorAuth(
|
|
41
|
+
uuid: string,
|
|
42
|
+
verifier: string,
|
|
43
|
+
): Promise<{ accessToken: string; refreshToken: string }> {
|
|
44
|
+
let delay = POLL_BASE_DELAY;
|
|
45
|
+
let consecutiveErrors = 0;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
48
|
+
await sleep(delay);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
|
|
52
|
+
|
|
53
|
+
if (response.status === 404) {
|
|
54
|
+
consecutiveErrors = 0;
|
|
55
|
+
delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (response.ok) {
|
|
60
|
+
const data = (await response.json()) as {
|
|
61
|
+
accessToken: string;
|
|
62
|
+
refreshToken: string;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
accessToken: data.accessToken,
|
|
66
|
+
refreshToken: data.refreshToken,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`Poll failed: ${response.status}`);
|
|
71
|
+
} catch (_error) {
|
|
72
|
+
consecutiveErrors++;
|
|
73
|
+
if (consecutiveErrors >= 3) {
|
|
74
|
+
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error("Cursor authentication polling timeout");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function loginCursor(
|
|
83
|
+
onAuthUrl: (url: string) => void,
|
|
84
|
+
onPollStart?: () => void,
|
|
85
|
+
): Promise<OAuthCredentials> {
|
|
86
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
87
|
+
|
|
88
|
+
onAuthUrl(loginUrl);
|
|
89
|
+
onPollStart?.();
|
|
90
|
+
|
|
91
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
92
|
+
|
|
93
|
+
const expiresAt = getTokenExpiry(accessToken);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
access: accessToken,
|
|
97
|
+
refresh: refreshToken,
|
|
98
|
+
expires: expiresAt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials> {
|
|
103
|
+
const response = await fetch(CURSOR_REFRESH_URL, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${apiKeyOrRefreshToken}`,
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: "{}",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const error = await response.text();
|
|
114
|
+
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = (await response.json()) as {
|
|
118
|
+
accessToken: string;
|
|
119
|
+
refreshToken: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const expiresAt = getTokenExpiry(data.accessToken);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
access: data.accessToken,
|
|
126
|
+
refresh: data.refreshToken || apiKeyOrRefreshToken,
|
|
127
|
+
expires: expiresAt,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getTokenExpiry(token: string): number {
|
|
132
|
+
try {
|
|
133
|
+
const [, payload] = token.split(".");
|
|
134
|
+
if (!payload) {
|
|
135
|
+
return Date.now() + 3600 * 1000;
|
|
136
|
+
}
|
|
137
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
138
|
+
if (decoded.exp) {
|
|
139
|
+
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore parsing errors
|
|
143
|
+
}
|
|
144
|
+
return Date.now() + 3600 * 1000;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isTokenExpiringSoon(token: string, thresholdSeconds = 300): boolean {
|
|
148
|
+
try {
|
|
149
|
+
const [, payload] = token.split(".");
|
|
150
|
+
if (!payload) return true;
|
|
151
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
152
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
153
|
+
return decoded.exp - currentTime < thresholdSeconds;
|
|
154
|
+
} catch {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
// Anthropic
|
|
13
13
|
export { loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
14
|
+
// Cursor
|
|
15
|
+
export {
|
|
16
|
+
generateCursorAuthParams,
|
|
17
|
+
isTokenExpiringSoon as isCursorTokenExpiringSoon,
|
|
18
|
+
loginCursor,
|
|
19
|
+
pollCursorAuth,
|
|
20
|
+
refreshCursorToken,
|
|
21
|
+
} from "./cursor";
|
|
14
22
|
// GitHub Copilot
|
|
15
23
|
export {
|
|
16
24
|
getGitHubCopilotBaseUrl,
|
|
@@ -41,6 +49,7 @@ export * from "./types";
|
|
|
41
49
|
// ============================================================================
|
|
42
50
|
|
|
43
51
|
import { refreshAnthropicToken } from "./anthropic";
|
|
52
|
+
import { refreshCursorToken } from "./cursor";
|
|
44
53
|
import { refreshGitHubCopilotToken } from "./github-copilot";
|
|
45
54
|
import { refreshAntigravityToken } from "./google-antigravity";
|
|
46
55
|
import { refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
@@ -83,6 +92,9 @@ export async function refreshOAuthToken(
|
|
|
83
92
|
case "openai-codex":
|
|
84
93
|
newCredentials = await refreshOpenAICodexToken(credentials.refresh);
|
|
85
94
|
break;
|
|
95
|
+
case "cursor":
|
|
96
|
+
newCredentials = await refreshCursorToken(credentials.refresh);
|
|
97
|
+
break;
|
|
86
98
|
default:
|
|
87
99
|
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
88
100
|
}
|
|
@@ -153,5 +165,10 @@ export function getOAuthProviders(): OAuthProviderInfo[] {
|
|
|
153
165
|
name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
|
|
154
166
|
available: true,
|
|
155
167
|
},
|
|
168
|
+
{
|
|
169
|
+
id: "cursor",
|
|
170
|
+
name: "Cursor (Claude, GPT, etc.)",
|
|
171
|
+
available: true,
|
|
172
|
+
},
|
|
156
173
|
];
|
|
157
174
|
}
|