@rahularya01/pi-cursor 1.1.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.
- package/CHANGELOG.md +11 -0
- package/dist/index.js +27 -0
- package/package.json +8 -5
- package/src/auth/cli-credentials.ts +0 -193
- package/src/auth/consent.ts +0 -27
- package/src/auth/index.ts +0 -22
- package/src/auth/oauth.ts +0 -214
- package/src/client/bridge.ts +0 -206
- package/src/client/cursor-wire.ts +0 -212
- package/src/client/index.ts +0 -19
- package/src/diagnostics/diagnostics.ts +0 -79
- package/src/diagnostics/index.ts +0 -1
- package/src/index.ts +0 -1436
- package/src/models/catalog.json +0 -1163
- package/src/models/index.ts +0 -2
- package/src/proto/agent_pb.ts +0 -15294
- package/src/stream/config.ts +0 -69
- package/src/stream/context-normalize.ts +0 -104
- package/src/stream/index.ts +0 -42
- package/src/stream/model-routing.ts +0 -100
- package/src/stream/native-core.ts +0 -5401
- package/src/stream/protocol.ts +0 -41
- package/src/stream/recovery.ts +0 -454
- package/src/usage.ts +0 -262
- package/src/utils/index.ts +0 -2
- package/src/utils/security.ts +0 -65
- package/src/utils/util.ts +0 -19
- package/tsconfig.json +0 -21
- /package/{src/client → dist}/h2-bridge.mjs +0 -0
|
@@ -1,193 +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 { systemCredentialsAllowed } from "./consent.js";
|
|
6
|
-
import { getCursorAccessTokenFromEnv, getTokenExpiry, refreshCursorToken } from "./oauth.js";
|
|
7
|
-
|
|
8
|
-
export type CredentialSource =
|
|
9
|
-
| "env"
|
|
10
|
-
| "cli_keychain"
|
|
11
|
-
| "cli_keychain_refresh"
|
|
12
|
-
| "ide_vscdb"
|
|
13
|
-
| "ide_vscdb_refresh"
|
|
14
|
-
| "pi_oauth"
|
|
15
|
-
| "pi_oauth_refresh";
|
|
16
|
-
|
|
17
|
-
export interface CursorTokenResult {
|
|
18
|
-
accessToken: string;
|
|
19
|
-
source: CredentialSource;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Reads token from macOS Keychain (security CLI).
|
|
24
|
-
*/
|
|
25
|
-
export async function getCursorKeychainToken(): Promise<CursorTokenResult | undefined> {
|
|
26
|
-
if (platform() !== "darwin") return undefined;
|
|
27
|
-
|
|
28
|
-
let accessToken: string | undefined;
|
|
29
|
-
let refreshToken: string | undefined;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const rawAccess = execFileSync(
|
|
33
|
-
"security",
|
|
34
|
-
["find-generic-password", "-s", "cursor-access-token", "-a", "cursor-user", "-w"],
|
|
35
|
-
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 },
|
|
36
|
-
).trim();
|
|
37
|
-
if (rawAccess) accessToken = rawAccess;
|
|
38
|
-
} catch {
|
|
39
|
-
// Keychain item not found or error
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const rawRefresh = execFileSync(
|
|
44
|
-
"security",
|
|
45
|
-
["find-generic-password", "-s", "cursor-refresh-token", "-a", "cursor-user", "-w"],
|
|
46
|
-
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 },
|
|
47
|
-
).trim();
|
|
48
|
-
if (rawRefresh) refreshToken = rawRefresh;
|
|
49
|
-
} catch {
|
|
50
|
-
// Keychain item not found or error
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (accessToken && Date.now() < getTokenExpiry(accessToken)) {
|
|
54
|
-
return { accessToken, source: "cli_keychain" };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (refreshToken) {
|
|
58
|
-
try {
|
|
59
|
-
const refreshed = await refreshCursorToken(refreshToken);
|
|
60
|
-
return { accessToken: refreshed.access, source: "cli_keychain_refresh" };
|
|
61
|
-
} catch {
|
|
62
|
-
// Refresh failed
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function getDatabaseSync() {
|
|
70
|
-
try {
|
|
71
|
-
const mod = await import("node:sqlite");
|
|
72
|
-
return mod.DatabaseSync;
|
|
73
|
-
} catch {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Reads token from Cursor IDE state.vscdb.
|
|
80
|
-
*/
|
|
81
|
-
export async function getCursorVscdbToken(): Promise<CursorTokenResult | undefined> {
|
|
82
|
-
const DatabaseSyncClass = await getDatabaseSync();
|
|
83
|
-
if (!DatabaseSyncClass) return undefined;
|
|
84
|
-
|
|
85
|
-
const dbPaths: string[] = [];
|
|
86
|
-
const home = homedir();
|
|
87
|
-
|
|
88
|
-
if (platform() === "darwin") {
|
|
89
|
-
dbPaths.push(join(home, "Library/Application Support/Cursor/User/globalStorage/state.vscdb"));
|
|
90
|
-
} else if (platform() === "win32") {
|
|
91
|
-
if (process.env.APPDATA) {
|
|
92
|
-
dbPaths.push(join(process.env.APPDATA, "Cursor/User/globalStorage/state.vscdb"));
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// Linux / WSL
|
|
96
|
-
dbPaths.push(join(home, ".config/Cursor/User/globalStorage/state.vscdb"));
|
|
97
|
-
// If running inside WSL, auto-detect Windows host Cursor credentials
|
|
98
|
-
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP || existsSync("/mnt/c/Users")) {
|
|
99
|
-
try {
|
|
100
|
-
const usersDir = "/mnt/c/Users";
|
|
101
|
-
if (existsSync(usersDir)) {
|
|
102
|
-
for (const user of readdirSync(usersDir)) {
|
|
103
|
-
if (user === "Public" || user === "Default" || user.startsWith(".")) continue;
|
|
104
|
-
dbPaths.push(
|
|
105
|
-
join(usersDir, user, "AppData/Roaming/Cursor/User/globalStorage/state.vscdb"),
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore read permission errors on Windows user profiles
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const dbPath of dbPaths) {
|
|
116
|
-
try {
|
|
117
|
-
const db = new DatabaseSyncClass(dbPath, { readOnly: true });
|
|
118
|
-
|
|
119
|
-
const accessRow = db
|
|
120
|
-
.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'")
|
|
121
|
-
.get() as { value?: string } | undefined;
|
|
122
|
-
const refreshRow = db
|
|
123
|
-
.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/refreshToken'")
|
|
124
|
-
.get() as { value?: string } | undefined;
|
|
125
|
-
|
|
126
|
-
db.close();
|
|
127
|
-
|
|
128
|
-
const accessToken = typeof accessRow?.value === "string" ? accessRow.value.trim() : undefined;
|
|
129
|
-
const refreshToken =
|
|
130
|
-
typeof refreshRow?.value === "string" ? refreshRow.value.trim() : undefined;
|
|
131
|
-
|
|
132
|
-
if (accessToken && Date.now() < getTokenExpiry(accessToken)) {
|
|
133
|
-
return { accessToken, source: "ide_vscdb" };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (refreshToken) {
|
|
137
|
-
try {
|
|
138
|
-
const refreshed = await refreshCursorToken(refreshToken);
|
|
139
|
-
return { accessToken: refreshed.access, source: "ide_vscdb_refresh" };
|
|
140
|
-
} catch {
|
|
141
|
-
// Refresh failed
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
} catch {
|
|
145
|
-
// Database missing or unreadable
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Full credential resolution cascade:
|
|
154
|
-
* 1. CURSOR_ACCESS_TOKEN env var
|
|
155
|
-
* 2. macOS Keychain (Cursor CLI) — gated by system-credential consent
|
|
156
|
-
* 3. Cursor IDE state.vscdb — gated by system-credential consent
|
|
157
|
-
*
|
|
158
|
-
* Opt out of Keychain/vscdb scraping with PI_CURSOR_SYSTEM_CREDENTIALS=0.
|
|
159
|
-
*/
|
|
160
|
-
export async function resolveSystemCursorAccessToken(options?: {
|
|
161
|
-
forceRefresh?: boolean;
|
|
162
|
-
}): Promise<CursorTokenResult | undefined> {
|
|
163
|
-
const envToken = getCursorAccessTokenFromEnv();
|
|
164
|
-
if (envToken) {
|
|
165
|
-
// Env tokens cannot be refreshed here; still prefer them when present.
|
|
166
|
-
if (!options?.forceRefresh || Date.now() < getTokenExpiry(envToken)) {
|
|
167
|
-
return { accessToken: envToken, source: "env" };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!systemCredentialsAllowed()) {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// forceRefresh: skip unexpired access-token short-circuit by re-reading sources
|
|
176
|
-
// (keychain/vscdb helpers already refresh when access is expired).
|
|
177
|
-
const keychainToken = await getCursorKeychainToken();
|
|
178
|
-
if (keychainToken) {
|
|
179
|
-
if (!options?.forceRefresh || keychainToken.source.endsWith("_refresh")) {
|
|
180
|
-
return keychainToken;
|
|
181
|
-
}
|
|
182
|
-
// Access still valid but forceRefresh requested — try refresh path via re-read.
|
|
183
|
-
// Keychain helper returns unexpired access first; force path falls through to vscdb/oauth.
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const vscdbToken = await getCursorVscdbToken();
|
|
187
|
-
if (vscdbToken) return vscdbToken;
|
|
188
|
-
|
|
189
|
-
// If forceRefresh and we only had unexpired keychain access, return it as last resort.
|
|
190
|
-
if (keychainToken) return keychainToken;
|
|
191
|
-
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
package/src/auth/consent.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Consent / opt-out for reusing Cursor CLI / IDE system credentials.
|
|
3
|
-
*
|
|
4
|
-
* Default: allow system credential reuse (Keychain + state.vscdb + WSL host).
|
|
5
|
-
* Opt out with PI_CURSOR_SYSTEM_CREDENTIALS=0|false|off|deny.
|
|
6
|
-
* Force-enable with PI_CURSOR_SYSTEM_CREDENTIALS=1|true|on|allow.
|
|
7
|
-
*/
|
|
8
|
-
export type SystemCredentialPolicy = "allow" | "deny";
|
|
9
|
-
|
|
10
|
-
export function resolveSystemCredentialPolicy(
|
|
11
|
-
envValue: string | undefined = process.env.PI_CURSOR_SYSTEM_CREDENTIALS,
|
|
12
|
-
): SystemCredentialPolicy {
|
|
13
|
-
const raw = envValue?.trim().toLowerCase();
|
|
14
|
-
if (!raw) return "allow";
|
|
15
|
-
if (raw === "0" || raw === "false" || raw === "off" || raw === "deny" || raw === "no") {
|
|
16
|
-
return "deny";
|
|
17
|
-
}
|
|
18
|
-
if (raw === "1" || raw === "true" || raw === "on" || raw === "allow" || raw === "yes") {
|
|
19
|
-
return "allow";
|
|
20
|
-
}
|
|
21
|
-
// Unknown values fail closed so misconfiguration never silently scrapes credentials.
|
|
22
|
-
return "deny";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function systemCredentialsAllowed(envValue?: string): boolean {
|
|
26
|
-
return resolveSystemCredentialPolicy(envValue) === "allow";
|
|
27
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,22 +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";
|
|
11
|
-
export {
|
|
12
|
-
resolveSystemCursorAccessToken,
|
|
13
|
-
getCursorKeychainToken,
|
|
14
|
-
getCursorVscdbToken,
|
|
15
|
-
type CredentialSource,
|
|
16
|
-
type CursorTokenResult,
|
|
17
|
-
} from "./cli-credentials.js";
|
|
18
|
-
export {
|
|
19
|
-
systemCredentialsAllowed,
|
|
20
|
-
resolveSystemCredentialPolicy,
|
|
21
|
-
type SystemCredentialPolicy,
|
|
22
|
-
} from "./consent.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
|
-
}
|
package/src/client/bridge.ts
DELETED
|
@@ -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
|
-
}
|