@rahularya01/pi-cursor 1.0.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/package.json +67 -0
- package/src/auth/cli-credentials.ts +168 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/oauth.ts +214 -0
- package/src/client/bridge.ts +206 -0
- package/src/client/cursor-wire.ts +212 -0
- package/src/client/h2-bridge.mjs +208 -0
- package/src/client/index.ts +19 -0
- package/src/diagnostics/diagnostics.ts +62 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/index.ts +1393 -0
- package/src/models/catalog.json +1163 -0
- package/src/models/index.ts +2 -0
- package/src/proto/agent_pb.ts +15294 -0
- package/src/stream/index.ts +11 -0
- package/src/stream/native-core.ts +5760 -0
- package/src/usage.ts +262 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/security.ts +65 -0
- package/src/utils/util.ts +19 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export interface CursorModelParameter {
|
|
2
|
+
id: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CursorParameterizedVariant {
|
|
7
|
+
parameters: CursorModelParameter[];
|
|
8
|
+
isMaxMode: boolean;
|
|
9
|
+
isDefaultMaxConfig?: boolean;
|
|
10
|
+
isDefaultNonMaxConfig?: boolean;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
displayNameOutsidePicker?: string;
|
|
13
|
+
variantStringRepresentation?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CursorParameterizedModel {
|
|
17
|
+
name: string;
|
|
18
|
+
clientDisplayName?: string;
|
|
19
|
+
serverModelName?: string;
|
|
20
|
+
supportsMaxMode?: boolean;
|
|
21
|
+
supportsNonMaxMode?: boolean;
|
|
22
|
+
supportsImages?: boolean;
|
|
23
|
+
contextTokenLimit?: number;
|
|
24
|
+
contextTokenLimitForMaxMode?: number;
|
|
25
|
+
variants: CursorParameterizedVariant[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function encodeVarint(value: number): number[] {
|
|
29
|
+
const out: number[] = [];
|
|
30
|
+
let v = value >>> 0;
|
|
31
|
+
while (v >= 0x80) {
|
|
32
|
+
out.push((v & 0x7f) | 0x80);
|
|
33
|
+
v >>>= 7;
|
|
34
|
+
}
|
|
35
|
+
out.push(v);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function encodeBoolField(fieldNo: number, value: boolean): number[] {
|
|
40
|
+
return [...encodeVarint(fieldNo << 3), value ? 1 : 0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function encodeAvailableModelsRequest(): Uint8Array {
|
|
44
|
+
// aiserver.v1.AvailableModelsRequest {
|
|
45
|
+
// optional bool use_model_parameters = 5;
|
|
46
|
+
// optional bool do_not_use_markdown = 7;
|
|
47
|
+
// }
|
|
48
|
+
return new Uint8Array([...encodeBoolField(5, true), ...encodeBoolField(7, true)]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface WireReader {
|
|
52
|
+
bytes: Uint8Array;
|
|
53
|
+
offset: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readVarint(reader: WireReader): number {
|
|
57
|
+
let result = 0;
|
|
58
|
+
let shift = 0;
|
|
59
|
+
while (reader.offset < reader.bytes.length) {
|
|
60
|
+
const byte = reader.bytes[reader.offset++]!;
|
|
61
|
+
// Avoid JS bitwise operators here: they truncate to 32 bits, but protobuf
|
|
62
|
+
// varints can legally carry 64-bit values on fields we merely skip.
|
|
63
|
+
if (shift < 53) result += (byte & 0x7f) * 2 ** shift;
|
|
64
|
+
if ((byte & 0x80) === 0) return result;
|
|
65
|
+
shift += 7;
|
|
66
|
+
if (shift >= 70) throw new Error("varint too long");
|
|
67
|
+
}
|
|
68
|
+
throw new Error("unexpected EOF while reading varint");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readLengthDelimited(reader: WireReader): Uint8Array {
|
|
72
|
+
const length = readVarint(reader);
|
|
73
|
+
if (!Number.isSafeInteger(length)) throw new Error("length-delimited size is too large");
|
|
74
|
+
const end = reader.offset + length;
|
|
75
|
+
if (end > reader.bytes.length) throw new Error("length-delimited field exceeds buffer");
|
|
76
|
+
const value = reader.bytes.subarray(reader.offset, end);
|
|
77
|
+
reader.offset = end;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function skipBytes(reader: WireReader, length: number): void {
|
|
82
|
+
const end = reader.offset + length;
|
|
83
|
+
if (end > reader.bytes.length) throw new Error("fixed-width field exceeds buffer");
|
|
84
|
+
reader.offset = end;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function skipWireField(reader: WireReader, wireType: number): void {
|
|
88
|
+
switch (wireType) {
|
|
89
|
+
case 0:
|
|
90
|
+
readVarint(reader);
|
|
91
|
+
return;
|
|
92
|
+
case 1:
|
|
93
|
+
skipBytes(reader, 8);
|
|
94
|
+
return;
|
|
95
|
+
case 2:
|
|
96
|
+
readLengthDelimited(reader);
|
|
97
|
+
return;
|
|
98
|
+
case 5:
|
|
99
|
+
skipBytes(reader, 4);
|
|
100
|
+
return;
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`unsupported wire type ${wireType}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decodeString(bytes: Uint8Array): string {
|
|
107
|
+
return new TextDecoder().decode(bytes);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function decodeModelParameter(bytes: Uint8Array): CursorModelParameter {
|
|
111
|
+
const reader: WireReader = { bytes, offset: 0 };
|
|
112
|
+
const parameter: CursorModelParameter = { id: "", value: "" };
|
|
113
|
+
while (reader.offset < bytes.length) {
|
|
114
|
+
const tag = readVarint(reader);
|
|
115
|
+
const fieldNo = tag >>> 3;
|
|
116
|
+
const wireType = tag & 0x7;
|
|
117
|
+
if (fieldNo === 1 && wireType === 2) parameter.id = decodeString(readLengthDelimited(reader));
|
|
118
|
+
else if (fieldNo === 2 && wireType === 2)
|
|
119
|
+
parameter.value = decodeString(readLengthDelimited(reader));
|
|
120
|
+
else skipWireField(reader, wireType);
|
|
121
|
+
}
|
|
122
|
+
return parameter;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodeParameterizedVariant(bytes: Uint8Array): CursorParameterizedVariant {
|
|
126
|
+
const reader: WireReader = { bytes, offset: 0 };
|
|
127
|
+
const variant: CursorParameterizedVariant = { parameters: [], isMaxMode: false };
|
|
128
|
+
while (reader.offset < bytes.length) {
|
|
129
|
+
const tag = readVarint(reader);
|
|
130
|
+
const fieldNo = tag >>> 3;
|
|
131
|
+
const wireType = tag & 0x7;
|
|
132
|
+
if (fieldNo === 1 && wireType === 2)
|
|
133
|
+
variant.parameters.push(decodeModelParameter(readLengthDelimited(reader)));
|
|
134
|
+
else if (fieldNo === 2 && wireType === 2)
|
|
135
|
+
variant.displayName = decodeString(readLengthDelimited(reader));
|
|
136
|
+
else if (fieldNo === 8 && wireType === 2)
|
|
137
|
+
variant.displayNameOutsidePicker = decodeString(readLengthDelimited(reader));
|
|
138
|
+
else if (fieldNo === 3 && wireType === 0) variant.isMaxMode = readVarint(reader) !== 0;
|
|
139
|
+
else if (fieldNo === 4 && wireType === 0) variant.isDefaultMaxConfig = readVarint(reader) !== 0;
|
|
140
|
+
else if (fieldNo === 5 && wireType === 0)
|
|
141
|
+
variant.isDefaultNonMaxConfig = readVarint(reader) !== 0;
|
|
142
|
+
else if (fieldNo === 9 && wireType === 2)
|
|
143
|
+
variant.variantStringRepresentation = decodeString(readLengthDelimited(reader));
|
|
144
|
+
else skipWireField(reader, wireType);
|
|
145
|
+
}
|
|
146
|
+
return variant;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function decodeParameterizedModel(bytes: Uint8Array): CursorParameterizedModel {
|
|
150
|
+
const reader: WireReader = { bytes, offset: 0 };
|
|
151
|
+
const model: CursorParameterizedModel = { name: "", variants: [] };
|
|
152
|
+
while (reader.offset < bytes.length) {
|
|
153
|
+
const tag = readVarint(reader);
|
|
154
|
+
const fieldNo = tag >>> 3;
|
|
155
|
+
const wireType = tag & 0x7;
|
|
156
|
+
if (fieldNo === 1 && wireType === 2) model.name = decodeString(readLengthDelimited(reader));
|
|
157
|
+
else if (fieldNo === 10 && wireType === 0) model.supportsImages = readVarint(reader) !== 0;
|
|
158
|
+
else if (fieldNo === 14 && wireType === 0) model.supportsMaxMode = readVarint(reader) !== 0;
|
|
159
|
+
else if (fieldNo === 19 && wireType === 0) model.supportsNonMaxMode = readVarint(reader) !== 0;
|
|
160
|
+
else if (fieldNo === 15 && wireType === 0) model.contextTokenLimit = readVarint(reader);
|
|
161
|
+
else if (fieldNo === 16 && wireType === 0)
|
|
162
|
+
model.contextTokenLimitForMaxMode = readVarint(reader);
|
|
163
|
+
else if (fieldNo === 17 && wireType === 2)
|
|
164
|
+
model.clientDisplayName = decodeString(readLengthDelimited(reader));
|
|
165
|
+
else if (fieldNo === 18 && wireType === 2)
|
|
166
|
+
model.serverModelName = decodeString(readLengthDelimited(reader));
|
|
167
|
+
else if (fieldNo === 30 && wireType === 2)
|
|
168
|
+
model.variants.push(decodeParameterizedVariant(readLengthDelimited(reader)));
|
|
169
|
+
else skipWireField(reader, wireType);
|
|
170
|
+
}
|
|
171
|
+
return model;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function decodeAvailableModelsResponse(bytes: Uint8Array): CursorParameterizedModel[] {
|
|
175
|
+
const reader: WireReader = { bytes, offset: 0 };
|
|
176
|
+
const models: CursorParameterizedModel[] = [];
|
|
177
|
+
while (reader.offset < bytes.length) {
|
|
178
|
+
const tag = readVarint(reader);
|
|
179
|
+
const fieldNo = tag >>> 3;
|
|
180
|
+
const wireType = tag & 0x7;
|
|
181
|
+
if (fieldNo === 2 && wireType === 2) {
|
|
182
|
+
const model = decodeParameterizedModel(readLengthDelimited(reader));
|
|
183
|
+
if (model.name) models.push(model);
|
|
184
|
+
} else {
|
|
185
|
+
skipWireField(reader, wireType);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return models;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// No generated schema for selectedContextBlob; emit raw wire format for the two
|
|
192
|
+
// fields Cursor actually reads: field 1 (repeated bytes) rootPromptMessagesJson
|
|
193
|
+
// refs, field 22 (string) clientName. blobId.length < 128 (SHA256 = 32 bytes).
|
|
194
|
+
export function buildSelectedContextBlob(
|
|
195
|
+
rootPromptBlobIds: Uint8Array[],
|
|
196
|
+
clientName: string,
|
|
197
|
+
): Uint8Array {
|
|
198
|
+
const parts: Uint8Array[] = [];
|
|
199
|
+
for (const blobId of rootPromptBlobIds) {
|
|
200
|
+
parts.push(new Uint8Array([0x0a, blobId.length, ...blobId]));
|
|
201
|
+
}
|
|
202
|
+
const clientBytes = new TextEncoder().encode(clientName);
|
|
203
|
+
parts.push(new Uint8Array([0xb2, 0x01, clientBytes.length, ...clientBytes]));
|
|
204
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
205
|
+
const result = new Uint8Array(total);
|
|
206
|
+
let offset = 0;
|
|
207
|
+
for (const p of parts) {
|
|
208
|
+
result.set(p, offset);
|
|
209
|
+
offset += p.length;
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dumb HTTP/2 bidirectional pipe for Cursor gRPC.
|
|
4
|
+
*
|
|
5
|
+
* Originally from https://github.com/ephraimduncan/opencode-cursor by Ephraim Duncan (MIT).
|
|
6
|
+
*
|
|
7
|
+
* Bun's node:http2 is broken. This Node script acts as a transparent
|
|
8
|
+
* HTTP/2 proxy: it opens a single bidirectional stream and ferries
|
|
9
|
+
* raw bytes between the parent process (via stdin/stdout) and Cursor.
|
|
10
|
+
*
|
|
11
|
+
* Protocol (length-prefixed framing over stdin/stdout):
|
|
12
|
+
* [4 bytes big-endian length][payload]
|
|
13
|
+
*
|
|
14
|
+
* First message on stdin is JSON config:
|
|
15
|
+
* { "accessToken": "...", "url": "...", "path": "...", "unary": false }
|
|
16
|
+
*
|
|
17
|
+
* When unary=true, the bridge uses application/proto (raw protobuf) instead
|
|
18
|
+
* of application/connect+proto (Connect streaming). The single stdin message
|
|
19
|
+
* is written as the request body and the stream is ended immediately.
|
|
20
|
+
* After config, subsequent stdin messages are raw bytes to write to the H2 stream.
|
|
21
|
+
* H2 response data is written to stdout using the same length-prefixed framing.
|
|
22
|
+
*/
|
|
23
|
+
import http2 from "node:http2";
|
|
24
|
+
import crypto from "node:crypto";
|
|
25
|
+
|
|
26
|
+
const CURSOR_CLIENT_VERSION = process.env.PI_CURSOR_CLIENT_VERSION || "cli-2026.05.01-eea359f";
|
|
27
|
+
|
|
28
|
+
/** Write one length-prefixed message to stdout. */
|
|
29
|
+
function writeMessage(data) {
|
|
30
|
+
const lenBuf = Buffer.alloc(4);
|
|
31
|
+
lenBuf.writeUInt32BE(data.length, 0);
|
|
32
|
+
process.stdout.write(lenBuf);
|
|
33
|
+
process.stdout.write(data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function connectEndStreamError(code, message) {
|
|
37
|
+
const payload = Buffer.from(JSON.stringify({ error: { code, message } }), "utf8");
|
|
38
|
+
const frame = Buffer.alloc(5 + payload.length);
|
|
39
|
+
frame[0] = 0b00000010;
|
|
40
|
+
frame.writeUInt32BE(payload.length, 1);
|
|
41
|
+
payload.copy(frame, 5);
|
|
42
|
+
return frame;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Buffered stdin reader ---
|
|
46
|
+
|
|
47
|
+
let stdinBuf = Buffer.alloc(0);
|
|
48
|
+
let stdinResolve = null;
|
|
49
|
+
let stdinEnded = false;
|
|
50
|
+
|
|
51
|
+
process.stdin.on("data", (chunk) => {
|
|
52
|
+
stdinBuf = Buffer.concat([stdinBuf, chunk]);
|
|
53
|
+
if (stdinResolve) {
|
|
54
|
+
const r = stdinResolve;
|
|
55
|
+
stdinResolve = null;
|
|
56
|
+
r();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.stdin.on("end", () => {
|
|
61
|
+
stdinEnded = true;
|
|
62
|
+
if (stdinResolve) {
|
|
63
|
+
const r = stdinResolve;
|
|
64
|
+
stdinResolve = null;
|
|
65
|
+
r();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function waitForData() {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
stdinResolve = resolve;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readExact(n) {
|
|
76
|
+
while (stdinBuf.length < n) {
|
|
77
|
+
if (stdinEnded) return null;
|
|
78
|
+
await waitForData();
|
|
79
|
+
}
|
|
80
|
+
const result = stdinBuf.subarray(0, n);
|
|
81
|
+
stdinBuf = stdinBuf.subarray(n);
|
|
82
|
+
return Buffer.from(result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readMessage() {
|
|
86
|
+
const lenBuf = await readExact(4);
|
|
87
|
+
if (!lenBuf) return null;
|
|
88
|
+
const len = lenBuf.readUInt32BE(0);
|
|
89
|
+
if (len === 0) return Buffer.alloc(0);
|
|
90
|
+
return readExact(len);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Main ---
|
|
94
|
+
|
|
95
|
+
const configBuf = await readMessage();
|
|
96
|
+
if (!configBuf) process.exit(1);
|
|
97
|
+
|
|
98
|
+
const config = JSON.parse(configBuf.toString("utf8"));
|
|
99
|
+
const { accessToken, url, path: rpcPath, unary } = config;
|
|
100
|
+
|
|
101
|
+
const client = http2.connect(url || "https://api2.cursor.sh");
|
|
102
|
+
|
|
103
|
+
// Guard against initial connection failure. Reset on any h2 activity
|
|
104
|
+
// so long-running agent conversations (with tool call round-trips) survive.
|
|
105
|
+
let timeout = setTimeout(killBridge, 30_000);
|
|
106
|
+
|
|
107
|
+
function resetTimeout() {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
timeout = setTimeout(killBridge, 120_000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function killBridge() {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
client.destroy();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
client.on("error", () => {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const headers = {
|
|
124
|
+
":method": "POST",
|
|
125
|
+
":path": rpcPath || "/agent.v1.AgentService/Run",
|
|
126
|
+
"content-type": unary ? "application/proto" : "application/connect+proto",
|
|
127
|
+
"connect-protocol-version": "1",
|
|
128
|
+
te: "trailers",
|
|
129
|
+
authorization: `Bearer ${accessToken}`,
|
|
130
|
+
"x-ghost-mode": "true",
|
|
131
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
132
|
+
"x-cursor-client-type": "cli",
|
|
133
|
+
"x-request-id": crypto.randomUUID(),
|
|
134
|
+
};
|
|
135
|
+
const h2Stream = client.request(headers);
|
|
136
|
+
let responseStatus = 0;
|
|
137
|
+
let responseStatusText = "";
|
|
138
|
+
const errorChunks = [];
|
|
139
|
+
const isErrorStatus = () => responseStatus !== 0 && (responseStatus < 200 || responseStatus >= 300);
|
|
140
|
+
|
|
141
|
+
h2Stream.on("response", (responseHeaders) => {
|
|
142
|
+
resetTimeout();
|
|
143
|
+
responseStatus = Number(responseHeaders[":status"] || 0);
|
|
144
|
+
responseStatusText =
|
|
145
|
+
responseHeaders["grpc-message"] || responseHeaders["connect-error-message"] || "";
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Forward H2 response data → stdout (length-prefixed)
|
|
149
|
+
h2Stream.on("data", (chunk) => {
|
|
150
|
+
resetTimeout();
|
|
151
|
+
if (isErrorStatus()) {
|
|
152
|
+
errorChunks.push(Buffer.from(chunk));
|
|
153
|
+
} else {
|
|
154
|
+
writeMessage(chunk);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
h2Stream.on("end", () => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
client.close();
|
|
161
|
+
if (isErrorStatus()) {
|
|
162
|
+
const body = Buffer.concat(errorChunks).toString("utf8").trim();
|
|
163
|
+
const detail = responseStatusText || body || "HTTP/2 upstream request failed";
|
|
164
|
+
writeMessage(
|
|
165
|
+
connectEndStreamError(`http_${responseStatus}`, `Cursor HTTP ${responseStatus}: ${detail}`),
|
|
166
|
+
);
|
|
167
|
+
setTimeout(() => process.exit(1), 100);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Give stdout time to flush
|
|
171
|
+
setTimeout(() => process.exit(0), 100);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
h2Stream.on("error", () => {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
client.close();
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Forward stdin → H2 stream (after config message)
|
|
181
|
+
if (unary) {
|
|
182
|
+
// Unary mode: read a single body message, write it, and end the stream.
|
|
183
|
+
const body = await readMessage();
|
|
184
|
+
if (body && body.length > 0 && !h2Stream.closed && !h2Stream.destroyed) {
|
|
185
|
+
h2Stream.end(body);
|
|
186
|
+
} else {
|
|
187
|
+
h2Stream.end();
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Streaming mode: forward all stdin messages as Connect frames.
|
|
191
|
+
(async () => {
|
|
192
|
+
while (true) {
|
|
193
|
+
const msg = await readMessage();
|
|
194
|
+
if (!msg || msg.length === 0) {
|
|
195
|
+
// EOF or zero-length = done writing
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
199
|
+
resetTimeout();
|
|
200
|
+
h2Stream.write(msg);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
205
|
+
h2Stream.end();
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
spawnBridge,
|
|
3
|
+
createConnectFrameParser,
|
|
4
|
+
frameConnectMessage,
|
|
5
|
+
parseConnectEndStream,
|
|
6
|
+
lpEncode,
|
|
7
|
+
type BridgeHandle,
|
|
8
|
+
type BridgeFactory,
|
|
9
|
+
type SpawnBridgeOptions,
|
|
10
|
+
} from "./bridge.js";
|
|
11
|
+
export {
|
|
12
|
+
encodeAvailableModelsRequest,
|
|
13
|
+
decodeAvailableModelsResponse,
|
|
14
|
+
buildSelectedContextBlob,
|
|
15
|
+
type CursorModelParameter,
|
|
16
|
+
type CursorParameterizedModel,
|
|
17
|
+
type CursorParameterizedVariant,
|
|
18
|
+
} from "./cursor-wire.js";
|
|
19
|
+
export { getCursorAgentUrl } from "../stream/native-core.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { redactSecrets } from "../utils/security.js";
|
|
3
|
+
|
|
4
|
+
export type DiagnosticsSnapshot = {
|
|
5
|
+
status?: number;
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
resolvedRuntimeModel?: string;
|
|
10
|
+
availableModels?: string;
|
|
11
|
+
matchedModelDebug?: string;
|
|
12
|
+
lastRpc?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const storage = new AsyncLocalStorage<DiagnosticsSnapshot>();
|
|
16
|
+
let lastSnapshot: DiagnosticsSnapshot = {};
|
|
17
|
+
|
|
18
|
+
function currentBag(): DiagnosticsSnapshot {
|
|
19
|
+
return storage.getStore() ?? lastSnapshot;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runWithDiagnostics<T>(fn: () => Promise<T>): Promise<T> {
|
|
23
|
+
const bag: DiagnosticsSnapshot = {};
|
|
24
|
+
return storage.run(bag, async () => {
|
|
25
|
+
try {
|
|
26
|
+
return await fn();
|
|
27
|
+
} finally {
|
|
28
|
+
lastSnapshot = { ...bag };
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getLastDiagnostics(): Readonly<DiagnosticsSnapshot> {
|
|
34
|
+
return lastSnapshot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setLastStatus(status: number | undefined): void {
|
|
38
|
+
currentBag().status = status;
|
|
39
|
+
}
|
|
40
|
+
export function setLastEndpoint(endpoint: string | undefined): void {
|
|
41
|
+
currentBag().endpoint = endpoint;
|
|
42
|
+
}
|
|
43
|
+
export function setLastError(error: string | undefined): void {
|
|
44
|
+
currentBag().error = error === undefined ? undefined : redactSecrets(error).slice(0, 800);
|
|
45
|
+
}
|
|
46
|
+
export function setLastResolvedRuntimeModel(model: string | undefined): void {
|
|
47
|
+
currentBag().resolvedRuntimeModel = model;
|
|
48
|
+
}
|
|
49
|
+
export function setLastAvailableModels(models: string | undefined): void {
|
|
50
|
+
currentBag().availableModels = models;
|
|
51
|
+
}
|
|
52
|
+
export function setLastRpc(rpc: string | undefined): void {
|
|
53
|
+
currentBag().lastRpc = rpc;
|
|
54
|
+
}
|
|
55
|
+
export function setLastMatchedModelDebug(debug: string | undefined): void {
|
|
56
|
+
currentBag().matchedModelDebug =
|
|
57
|
+
debug === undefined ? undefined : redactSecrets(debug).slice(0, 1200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resetDiagnosticsForTests(): void {
|
|
61
|
+
lastSnapshot = {};
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./diagnostics.js";
|