@rahularya01/pi-cursor 1.0.0 → 1.2.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.
@@ -1,168 +0,0 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import { execFileSync } from "node:child_process";
3
- import { homedir, platform } from "node:os";
4
- import { join } from "node:path";
5
- import { getCursorAccessTokenFromEnv, getTokenExpiry, refreshCursorToken } from "./oauth.js";
6
-
7
- export type CredentialSource =
8
- | "env"
9
- | "cli_keychain"
10
- | "cli_keychain_refresh"
11
- | "ide_vscdb"
12
- | "ide_vscdb_refresh"
13
- | "pi_oauth"
14
- | "pi_oauth_refresh";
15
-
16
- export interface CursorTokenResult {
17
- accessToken: string;
18
- source: CredentialSource;
19
- }
20
-
21
- /**
22
- * Reads token from macOS Keychain (security CLI).
23
- */
24
- export async function getCursorKeychainToken(): Promise<CursorTokenResult | undefined> {
25
- if (platform() !== "darwin") return undefined;
26
-
27
- let accessToken: string | undefined;
28
- let refreshToken: string | undefined;
29
-
30
- try {
31
- const rawAccess = execFileSync(
32
- "security",
33
- ["find-generic-password", "-s", "cursor-access-token", "-a", "cursor-user", "-w"],
34
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 },
35
- ).trim();
36
- if (rawAccess) accessToken = rawAccess;
37
- } catch {
38
- // Keychain item not found or error
39
- }
40
-
41
- try {
42
- const rawRefresh = execFileSync(
43
- "security",
44
- ["find-generic-password", "-s", "cursor-refresh-token", "-a", "cursor-user", "-w"],
45
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 },
46
- ).trim();
47
- if (rawRefresh) refreshToken = rawRefresh;
48
- } catch {
49
- // Keychain item not found or error
50
- }
51
-
52
- if (accessToken && Date.now() < getTokenExpiry(accessToken)) {
53
- return { accessToken, source: "cli_keychain" };
54
- }
55
-
56
- if (refreshToken) {
57
- try {
58
- const refreshed = await refreshCursorToken(refreshToken);
59
- return { accessToken: refreshed.access, source: "cli_keychain_refresh" };
60
- } catch {
61
- // Refresh failed
62
- }
63
- }
64
-
65
- return undefined;
66
- }
67
-
68
- async function getDatabaseSync() {
69
- try {
70
- const mod = await import("node:sqlite");
71
- return mod.DatabaseSync;
72
- } catch {
73
- return undefined;
74
- }
75
- }
76
-
77
- /**
78
- * Reads token from Cursor IDE state.vscdb.
79
- */
80
- export async function getCursorVscdbToken(): Promise<CursorTokenResult | undefined> {
81
- const DatabaseSyncClass = await getDatabaseSync();
82
- if (!DatabaseSyncClass) return undefined;
83
-
84
- const dbPaths: string[] = [];
85
- const home = homedir();
86
-
87
- if (platform() === "darwin") {
88
- dbPaths.push(join(home, "Library/Application Support/Cursor/User/globalStorage/state.vscdb"));
89
- } else if (platform() === "win32") {
90
- if (process.env.APPDATA) {
91
- dbPaths.push(join(process.env.APPDATA, "Cursor/User/globalStorage/state.vscdb"));
92
- }
93
- } else {
94
- // Linux / WSL
95
- dbPaths.push(join(home, ".config/Cursor/User/globalStorage/state.vscdb"));
96
- // If running inside WSL, auto-detect Windows host Cursor credentials
97
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP || existsSync("/mnt/c/Users")) {
98
- try {
99
- const usersDir = "/mnt/c/Users";
100
- if (existsSync(usersDir)) {
101
- for (const user of readdirSync(usersDir)) {
102
- if (user === "Public" || user === "Default" || user.startsWith(".")) continue;
103
- dbPaths.push(
104
- join(usersDir, user, "AppData/Roaming/Cursor/User/globalStorage/state.vscdb"),
105
- );
106
- }
107
- }
108
- } catch {
109
- // Ignore read permission errors on Windows user profiles
110
- }
111
- }
112
- }
113
-
114
- for (const dbPath of dbPaths) {
115
- try {
116
- const db = new DatabaseSyncClass(dbPath, { readOnly: true });
117
-
118
- const accessRow = db
119
- .prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'")
120
- .get() as { value?: string } | undefined;
121
- const refreshRow = db
122
- .prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/refreshToken'")
123
- .get() as { value?: string } | undefined;
124
-
125
- db.close();
126
-
127
- const accessToken = typeof accessRow?.value === "string" ? accessRow.value.trim() : undefined;
128
- const refreshToken =
129
- typeof refreshRow?.value === "string" ? refreshRow.value.trim() : undefined;
130
-
131
- if (accessToken && Date.now() < getTokenExpiry(accessToken)) {
132
- return { accessToken, source: "ide_vscdb" };
133
- }
134
-
135
- if (refreshToken) {
136
- try {
137
- const refreshed = await refreshCursorToken(refreshToken);
138
- return { accessToken: refreshed.access, source: "ide_vscdb_refresh" };
139
- } catch {
140
- // Refresh failed
141
- }
142
- }
143
- } catch {
144
- // Database missing or unreadable
145
- }
146
- }
147
-
148
- return undefined;
149
- }
150
-
151
- /**
152
- * Full credential resolution cascade:
153
- * 1. CURSOR_ACCESS_TOKEN env var
154
- * 2. macOS Keychain (Cursor CLI)
155
- * 3. Cursor IDE state.vscdb
156
- */
157
- export async function resolveSystemCursorAccessToken(): Promise<CursorTokenResult | undefined> {
158
- const envToken = getCursorAccessTokenFromEnv();
159
- if (envToken) return { accessToken: envToken, source: "env" };
160
-
161
- const keychainToken = await getCursorKeychainToken();
162
- if (keychainToken) return keychainToken;
163
-
164
- const vscdbToken = await getCursorVscdbToken();
165
- if (vscdbToken) return vscdbToken;
166
-
167
- return undefined;
168
- }
package/src/auth/index.ts DELETED
@@ -1,10 +0,0 @@
1
- export {
2
- generateCursorAuthParams,
3
- pollCursorAuth,
4
- refreshCursorToken,
5
- getTokenExpiry,
6
- getCursorAccessTokenFromEnv,
7
- createCursorAuthClient,
8
- type CursorAuthParams,
9
- type CursorCredentials,
10
- } from "./oauth.js";
package/src/auth/oauth.ts DELETED
@@ -1,214 +0,0 @@
1
- /**
2
- * Cursor OAuth authentication via PKCE.
3
- *
4
- * Flow:
5
- * 1. Generate PKCE verifier + challenge
6
- * 2. Open browser to cursor.com/loginDeepControl
7
- * 3. Poll api2.cursor.sh/auth/poll until tokens arrive
8
- * 4. Refresh via api2.cursor.sh/auth/exchange_user_api_key
9
- *
10
- * Based on https://github.com/ephraimduncan/opencode-cursor by Ephraim Duncan.
11
- */
12
-
13
- const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
14
- const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
15
- const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
16
- const POLL_MAX_ATTEMPTS = 150;
17
- const POLL_BASE_DELAY = 1000;
18
- const POLL_MAX_DELAY = 10_000;
19
- const POLL_BACKOFF_MULTIPLIER = 1.2;
20
-
21
- // ── PKCE ──
22
-
23
- async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
24
- const verifierBytes = new Uint8Array(96);
25
- crypto.getRandomValues(verifierBytes);
26
- const verifier = Buffer.from(verifierBytes).toString("base64url");
27
-
28
- const data = new TextEncoder().encode(verifier);
29
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
30
- const challenge = Buffer.from(hashBuffer).toString("base64url");
31
-
32
- return { verifier, challenge };
33
- }
34
-
35
- // ── Login params ──
36
-
37
- export interface CursorAuthParams {
38
- verifier: string;
39
- challenge: string;
40
- uuid: string;
41
- loginUrl: string;
42
- }
43
-
44
- export interface CursorAuthClientDependencies {
45
- fetch?: typeof fetch;
46
- generatePkce?: () => Promise<{ verifier: string; challenge: string; uuid?: string }>;
47
- randomUUID?: () => string;
48
- sleep?: (ms: number) => Promise<void>;
49
- }
50
-
51
- export interface CursorAuthLoginCallbacks {
52
- onAuth(payload: { url: string }): void | Promise<void>;
53
- }
54
-
55
- export function createCursorAuthClient(deps: CursorAuthClientDependencies = {}) {
56
- const fetchImpl = deps.fetch ?? fetch;
57
- const sleepImpl =
58
- deps.sleep ??
59
- ((ms: number) =>
60
- new Promise<void>((resolve) => {
61
- setTimeout(resolve, ms);
62
- }));
63
- const generatePkceImpl: () => Promise<{ verifier: string; challenge: string; uuid?: string }> =
64
- deps.generatePkce ?? generatePKCE;
65
- const randomUUIDImpl = deps.randomUUID ?? (() => crypto.randomUUID());
66
-
67
- async function generateParams(): Promise<CursorAuthParams> {
68
- const { verifier, challenge, uuid: injectedUuid } = await generatePkceImpl();
69
- const uuid = injectedUuid ?? randomUUIDImpl();
70
-
71
- const params = new URLSearchParams({
72
- challenge,
73
- uuid,
74
- mode: "login",
75
- redirectTarget: "cli",
76
- });
77
-
78
- const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
79
- return { verifier, challenge, uuid, loginUrl };
80
- }
81
-
82
- async function poll(
83
- uuid: string,
84
- verifier: string,
85
- ): Promise<{ accessToken: string; refreshToken: string }> {
86
- let delay = POLL_BASE_DELAY;
87
- let consecutiveErrors = 0;
88
-
89
- for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
90
- await sleepImpl(delay);
91
-
92
- try {
93
- const response = await fetchImpl(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
94
-
95
- if (response.status === 404) {
96
- consecutiveErrors = 0;
97
- delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
98
- continue;
99
- }
100
-
101
- if (response.ok) {
102
- const data = (await response.json()) as {
103
- accessToken: string;
104
- refreshToken: string;
105
- };
106
- return {
107
- accessToken: data.accessToken,
108
- refreshToken: data.refreshToken,
109
- };
110
- }
111
-
112
- throw new Error(`Poll failed: ${response.status}`);
113
- } catch {
114
- consecutiveErrors++;
115
- if (consecutiveErrors >= 3) {
116
- throw new Error("Too many consecutive errors during Cursor auth polling");
117
- }
118
- }
119
- }
120
-
121
- throw new Error("Cursor authentication polling timeout");
122
- }
123
-
124
- async function refreshToken(refreshToken: string): Promise<CursorCredentials> {
125
- const response = await fetchImpl(CURSOR_REFRESH_URL, {
126
- method: "POST",
127
- headers: {
128
- Authorization: `Bearer ${refreshToken}`,
129
- "Content-Type": "application/json",
130
- },
131
- body: "{}",
132
- });
133
-
134
- if (!response.ok) {
135
- const error = await response.text();
136
- throw new Error(`Cursor token refresh failed: ${error}`);
137
- }
138
-
139
- const data = (await response.json()) as {
140
- accessToken: string;
141
- refreshToken: string;
142
- };
143
-
144
- return {
145
- access: data.accessToken,
146
- refresh: data.refreshToken || refreshToken,
147
- expires: getTokenExpiry(data.accessToken),
148
- };
149
- }
150
-
151
- async function login(callbacks: CursorAuthLoginCallbacks): Promise<CursorCredentials> {
152
- const { verifier, uuid, loginUrl } = await generateParams();
153
- await callbacks.onAuth({ url: loginUrl });
154
- const { accessToken, refreshToken } = await poll(uuid, verifier);
155
- return {
156
- access: accessToken,
157
- refresh: refreshToken,
158
- expires: getTokenExpiry(accessToken),
159
- };
160
- }
161
-
162
- return {
163
- generateParams,
164
- login,
165
- poll,
166
- refreshToken,
167
- };
168
- }
169
-
170
- export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
171
- return createCursorAuthClient().generateParams();
172
- }
173
-
174
- // ── Poll for auth completion ──
175
-
176
- export async function pollCursorAuth(
177
- uuid: string,
178
- verifier: string,
179
- ): Promise<{ accessToken: string; refreshToken: string }> {
180
- return createCursorAuthClient().poll(uuid, verifier);
181
- }
182
-
183
- // ── Token refresh ──
184
-
185
- export interface CursorCredentials {
186
- access: string;
187
- refresh: string;
188
- expires: number;
189
- }
190
-
191
- export async function refreshCursorToken(refreshToken: string): Promise<CursorCredentials> {
192
- return createCursorAuthClient().refreshToken(refreshToken);
193
- }
194
-
195
- // ── JWT expiry extraction ──
196
-
197
- export function getCursorAccessTokenFromEnv(): string | undefined {
198
- const token = process.env.CURSOR_ACCESS_TOKEN?.trim();
199
- return token || undefined;
200
- }
201
-
202
- export function getTokenExpiry(token: string): number {
203
- try {
204
- const parts = token.split(".");
205
- if (parts.length !== 3 || !parts[1]) {
206
- return Date.now() + 3600 * 1000;
207
- }
208
- const decoded = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
209
- if (decoded && typeof decoded === "object" && typeof decoded.exp === "number") {
210
- return decoded.exp * 1000 - 5 * 60 * 1000;
211
- }
212
- } catch {}
213
- return Date.now() + 3600 * 1000;
214
- }
@@ -1,206 +0,0 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
2
- import { resolve as pathResolve, dirname } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const CURSOR_API_URL = "https://api2.cursor.sh";
6
- const CONNECT_END_STREAM_FLAG = 0b00000010;
7
- const BRIDGE_PATH = pathResolve(dirname(fileURLToPath(import.meta.url)), "h2-bridge.mjs");
8
-
9
- export interface SpawnBridgeOptions {
10
- accessToken: string;
11
- rpcPath: string;
12
- url?: string;
13
- unary?: boolean;
14
- }
15
-
16
- export interface BridgeHandle {
17
- proc: Pick<ChildProcess, "kill">;
18
- readonly alive: boolean;
19
- write(data: Uint8Array): void;
20
- end(): void;
21
- onData(cb: (chunk: Buffer) => void): void;
22
- onClose(cb: (code: number) => void): void;
23
- }
24
-
25
- export type BridgeFactory = (options: SpawnBridgeOptions) => BridgeHandle;
26
- export type BridgeDebugLog = (event: string, data?: Record<string, unknown>) => void;
27
-
28
- function noopDebugLog(): void {}
29
-
30
- type BridgeChildProcess = Pick<ChildProcess, "kill"> & {
31
- on(event: string | symbol, listener: (...args: any[]) => void): unknown;
32
- stdin?: NodeJS.WritableStream | null;
33
- stdout?: NodeJS.ReadableStream | null;
34
- };
35
-
36
- export function lpEncode(data: Uint8Array): Buffer {
37
- const buf = Buffer.alloc(4 + data.length);
38
- buf.writeUInt32BE(data.length, 0);
39
- buf.set(data, 4);
40
- return buf;
41
- }
42
-
43
- export function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
44
- const frame = Buffer.alloc(5 + data.length);
45
- frame[0] = flags;
46
- frame.writeUInt32BE(data.length, 1);
47
- frame.set(data, 5);
48
- return frame;
49
- }
50
-
51
- export function spawnBridge(
52
- options: SpawnBridgeOptions,
53
- debugLog: BridgeDebugLog = noopDebugLog,
54
- ): BridgeHandle {
55
- debugLog("bridge.spawn", {
56
- rpcPath: options.rpcPath,
57
- url: options.url ?? CURSOR_API_URL,
58
- unary: options.unary ?? false,
59
- cursorClientVersion: process.env.PI_CURSOR_CLIENT_VERSION || "cli-2026.05.01-eea359f",
60
- });
61
- const proc = spawn(process.execPath, [BRIDGE_PATH], {
62
- stdio: ["pipe", "pipe", "ignore"],
63
- });
64
-
65
- return createBridgeHandleForChild(proc, options, debugLog);
66
- }
67
-
68
- function createBridgeHandleForChild(
69
- proc: BridgeChildProcess,
70
- options: SpawnBridgeOptions,
71
- debugLog: BridgeDebugLog = noopDebugLog,
72
- ): BridgeHandle {
73
- const stdin = proc.stdin;
74
- const stdout = proc.stdout;
75
-
76
- const cbs = {
77
- data: null as ((chunk: Buffer) => void) | null,
78
- close: null as ((code: number) => void) | null,
79
- };
80
-
81
- let exited = false;
82
- let exitCode = 1;
83
- let stdinClosed = !stdin;
84
- const markStdinClosed = (err?: unknown): void => {
85
- stdinClosed = true;
86
- if (err) {
87
- debugLog("bridge.stdin_error", {
88
- code:
89
- typeof err === "object" && err !== null && "code" in err
90
- ? String((err as { code?: unknown }).code)
91
- : undefined,
92
- message: err instanceof Error ? err.message : String(err),
93
- });
94
- }
95
- };
96
- stdin?.on?.("error", markStdinClosed);
97
- stdin?.on?.("close", () => markStdinClosed());
98
- stdin?.on?.("finish", () => markStdinClosed());
99
-
100
- const safeWrite = (data: Uint8Array): void => {
101
- if (!stdin || stdinClosed) return;
102
- try {
103
- stdin.write(lpEncode(data));
104
- } catch (err) {
105
- markStdinClosed(err);
106
- }
107
- };
108
-
109
- const safeEnd = (): void => {
110
- if (!stdin || stdinClosed) return;
111
- try {
112
- stdin.end();
113
- stdinClosed = true;
114
- } catch (err) {
115
- markStdinClosed(err);
116
- }
117
- };
118
-
119
- const config = JSON.stringify({
120
- accessToken: options.accessToken,
121
- url: options.url ?? CURSOR_API_URL,
122
- path: options.rpcPath,
123
- unary: options.unary ?? false,
124
- });
125
- safeWrite(new TextEncoder().encode(config));
126
-
127
- let pending = Buffer.alloc(0);
128
- stdout?.on("data", (chunk: Buffer) => {
129
- pending = Buffer.concat([pending, chunk]);
130
- while (pending.length >= 4) {
131
- const len = pending.readUInt32BE(0);
132
- if (pending.length < 4 + len) break;
133
- const payload = pending.subarray(4, 4 + len);
134
- pending = pending.subarray(4 + len);
135
- cbs.data?.(Buffer.from(payload));
136
- }
137
- });
138
-
139
- proc.on("exit", (code) => {
140
- exited = true;
141
- exitCode = code ?? 1;
142
- debugLog("bridge.exit", { rpcPath: options.rpcPath, exitCode });
143
- cbs.close?.(exitCode);
144
- });
145
-
146
- return {
147
- proc,
148
- get alive() {
149
- return !exited;
150
- },
151
- write(data: Uint8Array) {
152
- safeWrite(data);
153
- },
154
- end() {
155
- safeWrite(new Uint8Array(0));
156
- safeEnd();
157
- },
158
- onData(cb: (chunk: Buffer) => void) {
159
- cbs.data = cb;
160
- },
161
- onClose(cb: (code: number) => void) {
162
- if (exited) {
163
- queueMicrotask(() => cb(exitCode));
164
- } else {
165
- cbs.close = cb;
166
- }
167
- },
168
- };
169
- }
170
-
171
- export const __testInternals = {
172
- createBridgeHandleForChild,
173
- };
174
-
175
- export function createConnectFrameParser(
176
- onMessage: (bytes: Uint8Array) => void,
177
- onEndStream: (bytes: Uint8Array) => void,
178
- ): (incoming: Buffer) => void {
179
- let pending = Buffer.alloc(0);
180
- return (incoming: Buffer) => {
181
- pending = Buffer.concat([pending, incoming]);
182
- while (pending.length >= 5) {
183
- const flags = pending[0]!;
184
- const msgLen = pending.readUInt32BE(1);
185
- if (pending.length < 5 + msgLen) break;
186
- const messageBytes = pending.subarray(5, 5 + msgLen);
187
- pending = pending.subarray(5 + msgLen);
188
- if (flags & CONNECT_END_STREAM_FLAG) onEndStream(messageBytes);
189
- else onMessage(messageBytes);
190
- }
191
- };
192
- }
193
-
194
- export function parseConnectEndStream(data: Uint8Array): Error | null {
195
- try {
196
- const payload = JSON.parse(new TextDecoder().decode(data));
197
- const error = payload?.error;
198
- if (error)
199
- return new Error(
200
- `Connect error ${error.code ?? "unknown"}: ${error.message ?? "Unknown error"}`,
201
- );
202
- return null;
203
- } catch {
204
- return new Error("Failed to parse Connect end stream");
205
- }
206
- }