@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/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
+ }
@@ -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
  }
@@ -13,7 +13,8 @@ export type OAuthProvider =
13
13
  | "github-copilot"
14
14
  | "google-gemini-cli"
15
15
  | "google-antigravity"
16
- | "openai-codex";
16
+ | "openai-codex"
17
+ | "cursor";
17
18
 
18
19
  export type OAuthPrompt = {
19
20
  message: string;