@offbynan/pi-cursor-provider 0.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/LICENSE +21 -0
- package/README.md +194 -0
- package/auth.ts +165 -0
- package/cursor-models-raw.json +583 -0
- package/h2-bridge.mjs +175 -0
- package/index.ts +714 -0
- package/package.json +58 -0
- package/proto/agent_pb.ts +15294 -0
- package/proxy.ts +2761 -0
package/h2-bridge.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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 = "cli-2026.01.09-231024f";
|
|
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
|
+
// --- Buffered stdin reader ---
|
|
37
|
+
|
|
38
|
+
let stdinBuf = Buffer.alloc(0);
|
|
39
|
+
let stdinResolve = null;
|
|
40
|
+
let stdinEnded = false;
|
|
41
|
+
|
|
42
|
+
process.stdin.on("data", (chunk) => {
|
|
43
|
+
stdinBuf = Buffer.concat([stdinBuf, chunk]);
|
|
44
|
+
if (stdinResolve) {
|
|
45
|
+
const r = stdinResolve;
|
|
46
|
+
stdinResolve = null;
|
|
47
|
+
r();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.stdin.on("end", () => {
|
|
52
|
+
stdinEnded = true;
|
|
53
|
+
if (stdinResolve) {
|
|
54
|
+
const r = stdinResolve;
|
|
55
|
+
stdinResolve = null;
|
|
56
|
+
r();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function waitForData() {
|
|
61
|
+
return new Promise((resolve) => { stdinResolve = resolve; });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readExact(n) {
|
|
65
|
+
while (stdinBuf.length < n) {
|
|
66
|
+
if (stdinEnded) return null;
|
|
67
|
+
await waitForData();
|
|
68
|
+
}
|
|
69
|
+
const result = stdinBuf.subarray(0, n);
|
|
70
|
+
stdinBuf = stdinBuf.subarray(n);
|
|
71
|
+
return Buffer.from(result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readMessage() {
|
|
75
|
+
const lenBuf = await readExact(4);
|
|
76
|
+
if (!lenBuf) return null;
|
|
77
|
+
const len = lenBuf.readUInt32BE(0);
|
|
78
|
+
if (len === 0) return Buffer.alloc(0);
|
|
79
|
+
return readExact(len);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Main ---
|
|
83
|
+
|
|
84
|
+
const configBuf = await readMessage();
|
|
85
|
+
if (!configBuf) process.exit(1);
|
|
86
|
+
|
|
87
|
+
const config = JSON.parse(configBuf.toString("utf8"));
|
|
88
|
+
const { accessToken, url, path: rpcPath, unary } = config;
|
|
89
|
+
|
|
90
|
+
const client = http2.connect(url || "https://api2.cursor.sh");
|
|
91
|
+
|
|
92
|
+
// Guard against initial connection failure. Reset on any h2 activity
|
|
93
|
+
// so long-running agent conversations (with tool call round-trips) survive.
|
|
94
|
+
let timeout = setTimeout(killBridge, 30_000);
|
|
95
|
+
|
|
96
|
+
function resetTimeout() {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
timeout = setTimeout(killBridge, 120_000);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function killBridge() {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
client.destroy();
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
client.on("error", () => {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const headers = {
|
|
113
|
+
":method": "POST",
|
|
114
|
+
":path": rpcPath || "/agent.v1.AgentService/Run",
|
|
115
|
+
"content-type": unary ? "application/proto" : "application/connect+proto",
|
|
116
|
+
te: "trailers",
|
|
117
|
+
authorization: `Bearer ${accessToken}`,
|
|
118
|
+
"x-ghost-mode": "true",
|
|
119
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
120
|
+
"x-cursor-client-type": "cli",
|
|
121
|
+
"x-request-id": crypto.randomUUID(),
|
|
122
|
+
};
|
|
123
|
+
if (!unary) {
|
|
124
|
+
headers["connect-protocol-version"] = "1";
|
|
125
|
+
}
|
|
126
|
+
const h2Stream = client.request(headers);
|
|
127
|
+
|
|
128
|
+
// Forward H2 response data → stdout (length-prefixed)
|
|
129
|
+
h2Stream.on("data", (chunk) => {
|
|
130
|
+
resetTimeout();
|
|
131
|
+
writeMessage(chunk);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
h2Stream.on("end", () => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
client.close();
|
|
137
|
+
// Give stdout time to flush
|
|
138
|
+
setTimeout(() => process.exit(0), 100);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
h2Stream.on("error", () => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
client.close();
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Forward stdin → H2 stream (after config message)
|
|
148
|
+
if (unary) {
|
|
149
|
+
// Unary mode: read a single body message, write it, and end the stream.
|
|
150
|
+
const body = await readMessage();
|
|
151
|
+
if (body && body.length > 0 && !h2Stream.closed && !h2Stream.destroyed) {
|
|
152
|
+
h2Stream.end(body);
|
|
153
|
+
} else {
|
|
154
|
+
h2Stream.end();
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Streaming mode: forward all stdin messages as Connect frames.
|
|
158
|
+
(async () => {
|
|
159
|
+
while (true) {
|
|
160
|
+
const msg = await readMessage();
|
|
161
|
+
if (!msg || msg.length === 0) {
|
|
162
|
+
// EOF or zero-length = done writing
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
166
|
+
resetTimeout();
|
|
167
|
+
h2Stream.write(msg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
172
|
+
h2Stream.end();
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
}
|