@rahularya01/pi-cursor 1.4.28 → 1.4.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahularya01/pi-cursor",
3
- "version": "1.4.28",
3
+ "version": "1.4.30",
4
4
  "description": "Native Cursor provider for Pi Coding Agent (OAuth + Connect/protobuf streamSimple)",
5
5
  "author": "Rahul Arya",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "type": "module",
20
20
  "main": "./dist/index.js",
21
21
  "engines": {
22
- "node": ">=22.19.0"
22
+ "bun": ">=1.4.0"
23
23
  },
24
24
  "keywords": [
25
25
  "pi-package",
@@ -35,27 +35,27 @@
35
35
  "dist"
36
36
  ],
37
37
  "scripts": {
38
- "build": "tsup",
39
- "prepare": "npm run build",
40
- "prepublishOnly": "npm run build",
38
+ "build": "bun scripts/build.ts",
39
+ "prepare": "bun run build",
40
+ "prepublishOnly": "bun run build",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "lint": "eslint src tests",
43
43
  "lint:fix": "eslint src tests --fix",
44
44
  "format": "prettier --write .",
45
45
  "format:check": "prettier --check .",
46
- "security-check": "tsx scripts/security-check.ts",
46
+ "security-check": "bun scripts/security-check.ts",
47
47
  "proto:gen": "buf generate",
48
- "proto:sync": "tsx scripts/proto-sync.ts",
49
- "proto:check": "tsx scripts/proto-check.ts",
50
- "test": "vitest run",
51
- "test:coverage": "vitest run --coverage",
52
- "test:watch": "vitest",
53
- "test:legacy": "tsx scripts/test-model-routing.ts && tsx scripts/test-thinking-levels.ts && tsx scripts/test-usage.ts && tsx scripts/test-context-mode-normalize.ts && tsx scripts/test-cli-auth.ts",
54
- "smoke:auth": "node --import tsx scripts/smoke-auth.mjs",
55
- "smoke:models": "node --import tsx scripts/smoke-models.mjs",
56
- "smoke:stream": "node --import tsx scripts/smoke-stream.mjs",
57
- "smoke:wire": "node --import tsx scripts/smoke-wire.mjs",
58
- "check": "npm run typecheck && npm run lint && npm run format:check && npm run security-check && npm run proto:check && npm test && npm run test:legacy"
48
+ "proto:sync": "bun scripts/proto-sync.ts",
49
+ "proto:check": "bun scripts/proto-check.ts",
50
+ "test": "bun test --timeout=15000",
51
+ "test:coverage": "bun test --coverage --timeout=15000 && bun scripts/coverage-threshold.ts",
52
+ "test:watch": "bun test --watch --timeout=15000",
53
+ "test:legacy": "bun scripts/test-model-routing.ts && bun scripts/test-thinking-levels.ts && bun scripts/test-usage.ts && bun scripts/test-context-mode-normalize.ts && bun scripts/test-cli-auth.ts",
54
+ "smoke:auth": "bun scripts/smoke-auth.mjs",
55
+ "smoke:models": "bun scripts/smoke-models.mjs",
56
+ "smoke:stream": "bun scripts/smoke-stream.mjs",
57
+ "smoke:wire": "bun scripts/smoke-wire.mjs",
58
+ "check": "bun run typecheck && bun run lint && bun run format:check && bun run security-check && bun run proto:check && bun run test && bun run test:legacy"
59
59
  },
60
60
  "pi": {
61
61
  "extensions": [
@@ -73,16 +73,14 @@
73
73
  "@bufbuild/buf": "^1.72.0",
74
74
  "@bufbuild/protoc-gen-es": "^2.13.0",
75
75
  "@eslint/js": "^10.0.1",
76
+ "@types/bun": "^1.4.0",
76
77
  "@types/node": "^26.1.1",
77
78
  "eslint": "^10.7.0",
78
79
  "eslint-config-prettier": "^10.1.8",
79
80
  "globals": "^17.7.0",
80
81
  "prettier": "^3.9.5",
81
- "tsup": "^8.5.1",
82
- "tsx": "^4.23.1",
83
82
  "typescript": "^6.0.3",
84
- "typescript-eslint": "^8.64.0",
85
- "@vitest/coverage-v8": "^3.2.4",
86
- "vitest": "^3.2.4"
87
- }
83
+ "typescript-eslint": "^8.64.0"
84
+ },
85
+ "packageManager": "bun@1.4.0"
88
86
  }
@@ -1,409 +0,0 @@
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, "persistent": true }
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
- *
21
- * Streaming with persistent=true (the default for chat) keeps the HTTP/2
22
- * session after a stream ends, writes a STREAM_DONE sentinel, and waits for
23
- * an `{"cmd":"open"}` stdin message to open the next Connect stream — so later
24
- * turns skip process spawn + TLS.
25
- *
26
- * After config, subsequent stdin messages are raw bytes to write to the H2 stream.
27
- * H2 response data is written to stdout using the same length-prefixed framing.
28
- */
29
- import http2 from "node:http2";
30
- import crypto from "node:crypto";
31
- import { once } from "node:events";
32
-
33
- const CURSOR_CLIENT_VERSION = process.env.PI_CURSOR_CLIENT_VERSION || "cli-2026.05.01-eea359f";
34
- const MAX_BRIDGE_MESSAGE_BYTES = 64 * 1024 * 1024;
35
- const MAX_ERROR_BODY_BYTES = 1024 * 1024;
36
-
37
- /** Write one length-prefixed message to stdout. */
38
- function writeMessage(data) {
39
- if (data.length > MAX_BRIDGE_MESSAGE_BYTES) {
40
- throw new Error(`bridge output exceeds ${MAX_BRIDGE_MESSAGE_BYTES} bytes`);
41
- }
42
- const lenBuf = Buffer.alloc(4);
43
- lenBuf.writeUInt32BE(data.length, 0);
44
- return process.stdout.write(Buffer.concat([lenBuf, data]));
45
- }
46
-
47
- function connectEndStreamError(code, message) {
48
- const payload = Buffer.from(JSON.stringify({ error: { code, message } }), "utf8");
49
- const frame = Buffer.alloc(5 + payload.length);
50
- frame[0] = 0b00000010;
51
- frame.writeUInt32BE(payload.length, 1);
52
- payload.copy(frame, 5);
53
- return frame;
54
- }
55
-
56
- // --- Buffered stdin reader ---
57
- //
58
- // Chunks are queued in an array and only concatenated once enough bytes have arrived to satisfy
59
- // a `readExact` call. Concatenating on every `data` event instead (`stdinBuf = Buffer.concat([
60
- // stdinBuf, chunk])`) is O(n^2) in the message size when a large message arrives split across
61
- // many small pipe reads, since every partial chunk re-copies everything buffered so far.
62
-
63
- let stdinChunks = [];
64
- let stdinLength = 0;
65
- let stdinResolve = null;
66
- let stdinEnded = false;
67
-
68
- process.stdin.on("data", (chunk) => {
69
- stdinChunks.push(chunk);
70
- stdinLength += chunk.length;
71
- if (stdinLength > MAX_BRIDGE_MESSAGE_BYTES + 4) {
72
- process.stderr.write("[h2-bridge] stdin buffer limit exceeded\n");
73
- process.exit(1);
74
- }
75
- if (stdinResolve) {
76
- const r = stdinResolve;
77
- stdinResolve = null;
78
- r();
79
- }
80
- });
81
-
82
- process.stdin.on("end", () => {
83
- stdinEnded = true;
84
- if (stdinResolve) {
85
- const r = stdinResolve;
86
- stdinResolve = null;
87
- r();
88
- }
89
- });
90
-
91
- function waitForData() {
92
- return new Promise((resolve) => {
93
- stdinResolve = resolve;
94
- });
95
- }
96
-
97
- async function readExact(n) {
98
- while (stdinLength < n) {
99
- if (stdinEnded) return null;
100
- await waitForData();
101
- }
102
- if (stdinChunks.length > 1) stdinChunks = [Buffer.concat(stdinChunks, stdinLength)];
103
- const buf = stdinChunks[0] ?? Buffer.alloc(0);
104
- const result = buf.subarray(0, n);
105
- const rest = buf.subarray(n);
106
- stdinChunks = rest.length > 0 ? [rest] : [];
107
- stdinLength = rest.length;
108
- return Buffer.from(result);
109
- }
110
-
111
- async function readMessage() {
112
- const lenBuf = await readExact(4);
113
- if (!lenBuf) return null;
114
- const len = lenBuf.readUInt32BE(0);
115
- if (len > MAX_BRIDGE_MESSAGE_BYTES) {
116
- throw new Error(`bridge input exceeds ${MAX_BRIDGE_MESSAGE_BYTES} bytes`);
117
- }
118
- if (len === 0) return Buffer.alloc(0);
119
- return readExact(len);
120
- }
121
-
122
- // --- Main ---
123
-
124
- const configBuf = await readMessage();
125
- if (!configBuf) process.exit(1);
126
-
127
- let config;
128
- try {
129
- config = JSON.parse(configBuf.toString("utf8"));
130
- } catch {
131
- process.stderr.write("[h2-bridge] invalid config JSON\n");
132
- process.exit(1);
133
- }
134
- if (!config || typeof config !== "object") {
135
- process.stderr.write("[h2-bridge] config must be a JSON object\n");
136
- process.exit(1);
137
- }
138
- const { accessToken, url, path: rpcPath, unary, persistent: persistentFlag } = config;
139
- const persistent = unary ? false : persistentFlag !== false;
140
- const STREAM_DONE_MAGIC = Buffer.from("PI_CURSOR_STREAM_DONE");
141
- // Connect timeout still protects against a hung first handshake (default 30s).
142
- // Activity idle is off by default (0) so long agent turns are not killed;
143
- // set idleTimeoutMs / PI_CURSOR_H2_IDLE_TIMEOUT_MS to re-enable a safety net.
144
- // Parent heartbeats every 5s also reset the timer when it is enabled.
145
- const connectTimeoutMs = optionalMs(config.connectTimeoutMs, 30_000);
146
- const idleTimeoutMs = optionalMs(config.idleTimeoutMs, 0);
147
-
148
- /** Parse ms; 0 means disabled. Invalid/missing uses fallback. */
149
- function optionalMs(value, fallback) {
150
- if (value === undefined || value === null || value === "") return fallback;
151
- const n = Number(value);
152
- if (!Number.isFinite(n) || n < 0) return fallback;
153
- if (n === 0) return 0;
154
- return Math.floor(n);
155
- }
156
-
157
- const client = http2.connect(url || "https://api2.cursor.sh");
158
-
159
- // HTTP/2 PING keeps intermediary/load-balancer sessions alive during long pure-thinking
160
- // stretches where no DATA frames flow. Cursor may otherwise GOAWAY the stream mid-turn.
161
- const pingEveryMs = optionalMs(config.pingIntervalMs, 20_000);
162
- let pingTimer = undefined;
163
- if (pingEveryMs > 0) {
164
- pingTimer = setInterval(() => {
165
- if (client.destroyed || client.closed) return;
166
- try {
167
- client.ping((err) => {
168
- if (err) {
169
- process.stderr.write(`[h2-bridge] ping failed: ${err.message}\n`);
170
- }
171
- });
172
- } catch (err) {
173
- process.stderr.write(
174
- `[h2-bridge] ping threw: ${err instanceof Error ? err.message : String(err)}\n`,
175
- );
176
- }
177
- }, pingEveryMs);
178
- pingTimer.unref?.();
179
- }
180
-
181
- // Optional watchdog: connect timeout until first activity, then activity idle.
182
- let timeout = undefined;
183
-
184
- function clearBridgeTimeout() {
185
- if (timeout) clearTimeout(timeout);
186
- timeout = undefined;
187
- }
188
-
189
- function armBridgeTimeout(ms) {
190
- clearBridgeTimeout();
191
- if (!ms || ms <= 0) return;
192
- timeout = setTimeout(killBridge, ms);
193
- }
194
-
195
- function resetTimeout() {
196
- // After first I/O, only the activity idle (if enabled) applies.
197
- armBridgeTimeout(idleTimeoutMs);
198
- }
199
-
200
- function killBridge() {
201
- clearBridgeTimeout();
202
- if (pingTimer) clearInterval(pingTimer);
203
- client.destroy();
204
- process.exit(1);
205
- }
206
-
207
- // Initial connect guard only (skipped when connectTimeoutMs is 0).
208
- armBridgeTimeout(connectTimeoutMs);
209
-
210
- client.on("error", (err) => {
211
- clearBridgeTimeout();
212
- if (pingTimer) clearInterval(pingTimer);
213
- process.stderr.write(
214
- `[h2-bridge] client error: ${err instanceof Error ? err.message : String(err)}\n`,
215
- );
216
- process.exit(1);
217
- });
218
-
219
- client.on("goaway", (errorCode, _lastStreamId, opaqueData) => {
220
- const opaque = opaqueData ? opaqueData.toString("utf8").slice(0, 200) : "";
221
- process.stderr.write(`[h2-bridge] GOAWAY errorCode=${errorCode} opaque=${opaque}\n`);
222
- // GOAWAY means the server closed the HTTP/2 connection gracefully.
223
- // Signal the parent with a retriable error frame so it can reconnect,
224
- // then exit with code 2 (reserved for retriable transport loss).
225
- clearBridgeTimeout();
226
- if (pingTimer) clearInterval(pingTimer);
227
- writeMessage(
228
- connectEndStreamError(
229
- "unavailable",
230
- `Cursor GOAWAY (errorCode=${errorCode}): upstream connection closed, retriable`,
231
- ),
232
- );
233
- setTimeout(() => process.exit(2), 100);
234
- });
235
-
236
- function requestHeaders(token) {
237
- return {
238
- ":method": "POST",
239
- ":path": rpcPath || "/agent.v1.AgentService/Run",
240
- "content-type": unary ? "application/proto" : "application/connect+proto",
241
- "connect-protocol-version": "1",
242
- te: "trailers",
243
- authorization: `Bearer ${token}`,
244
- "x-ghost-mode": "true",
245
- "x-cursor-client-version": CURSOR_CLIENT_VERSION,
246
- "x-cursor-client-type": "cli",
247
- "x-request-id": crypto.randomUUID(),
248
- };
249
- }
250
-
251
- function parseOpenCommand(msg) {
252
- if (!msg || msg.length === 0 || msg[0] !== 0x7b) return undefined;
253
- try {
254
- const parsed = JSON.parse(msg.toString("utf8"));
255
- if (parsed && parsed.cmd === "open") return parsed;
256
- } catch {
257
- // Binary Connect frames that happen to start with `{` are not open commands.
258
- }
259
- return undefined;
260
- }
261
-
262
- function attachStream(h2Stream) {
263
- let responseStatus = 0;
264
- let responseStatusText = "";
265
- const errorChunks = [];
266
- let errorBodyBytes = 0;
267
- const isErrorStatus = () => responseStatus !== 0 && (responseStatus < 200 || responseStatus >= 300);
268
-
269
- h2Stream.on("response", (responseHeaders) => {
270
- resetTimeout();
271
- responseStatus = Number(responseHeaders[":status"] || 0);
272
- responseStatusText =
273
- responseHeaders["grpc-message"] || responseHeaders["connect-error-message"] || "";
274
- });
275
-
276
- h2Stream.on("data", (chunk) => {
277
- resetTimeout();
278
- if (isErrorStatus()) {
279
- const remaining = MAX_ERROR_BODY_BYTES - errorBodyBytes;
280
- if (remaining > 0) {
281
- const kept = Buffer.from(chunk).subarray(0, remaining);
282
- errorChunks.push(kept);
283
- errorBodyBytes += kept.byteLength;
284
- }
285
- } else if (!writeMessage(chunk)) {
286
- h2Stream.pause();
287
- process.stdout.once("drain", () => h2Stream.resume());
288
- }
289
- });
290
-
291
- return new Promise((resolve) => {
292
- const finish = (result) => {
293
- resolve(result);
294
- };
295
-
296
- h2Stream.on("end", () => {
297
- if (isErrorStatus()) {
298
- const body = Buffer.concat(errorChunks).toString("utf8").trim();
299
- const detail = responseStatusText || body || "HTTP/2 upstream request failed";
300
- writeMessage(
301
- connectEndStreamError(`http_${responseStatus}`, `Cursor HTTP ${responseStatus}: ${detail}`),
302
- );
303
- finish({ ok: false, fatal: true });
304
- return;
305
- }
306
- finish({ ok: true, fatal: false });
307
- });
308
-
309
- h2Stream.on("error", (err) => {
310
- process.stderr.write(
311
- `[h2-bridge] stream error: ${err instanceof Error ? err.message : String(err)}\n`,
312
- );
313
- finish({ ok: false, fatal: true });
314
- });
315
- });
316
- }
317
-
318
- function shutdownClient(code) {
319
- clearBridgeTimeout();
320
- if (pingTimer) clearInterval(pingTimer);
321
- try {
322
- client.close();
323
- } catch {
324
- // Already closed.
325
- }
326
- setTimeout(() => process.exit(code), 100);
327
- }
328
-
329
- if (unary) {
330
- const h2Stream = client.request(requestHeaders(accessToken));
331
- const ended = attachStream(h2Stream);
332
- const body = await readMessage();
333
- if (body && body.length > 0 && !h2Stream.closed && !h2Stream.destroyed) {
334
- h2Stream.end(body);
335
- } else {
336
- h2Stream.end();
337
- }
338
- const result = await ended;
339
- shutdownClient(result.ok ? 0 : 1);
340
- } else {
341
- let currentStream = client.request(requestHeaders(accessToken));
342
- let currentEnded = attachStream(currentStream);
343
-
344
- currentEnded.then((result) => {
345
- if (!result.ok) {
346
- shutdownClient(1);
347
- return;
348
- }
349
- if (!persistent) {
350
- shutdownClient(0);
351
- return;
352
- }
353
- writeMessage(STREAM_DONE_MAGIC);
354
- currentStream = null;
355
- });
356
-
357
- (async () => {
358
- while (true) {
359
- const msg = await readMessage();
360
- if (!msg) {
361
- shutdownClient(0);
362
- return;
363
- }
364
- if (msg.length === 0) {
365
- if (currentStream && !currentStream.closed && !currentStream.destroyed) {
366
- currentStream.end();
367
- }
368
- if (!persistent) {
369
- shutdownClient(0);
370
- return;
371
- }
372
- continue;
373
- }
374
-
375
- const open = parseOpenCommand(msg);
376
- if (open) {
377
- if (client.destroyed || client.closed) {
378
- process.stderr.write("[h2-bridge] cannot open stream: client closed\n");
379
- shutdownClient(1);
380
- return;
381
- }
382
- const token = typeof open.accessToken === "string" && open.accessToken ? open.accessToken : accessToken;
383
- currentStream = client.request(requestHeaders(token));
384
- currentEnded = attachStream(currentStream);
385
- currentEnded.then((result) => {
386
- if (!result.ok) {
387
- shutdownClient(1);
388
- return;
389
- }
390
- writeMessage(STREAM_DONE_MAGIC);
391
- currentStream = null;
392
- });
393
- continue;
394
- }
395
-
396
- if (currentStream && !currentStream.closed && !currentStream.destroyed) {
397
- resetTimeout();
398
- if (!currentStream.write(msg)) {
399
- try {
400
- await once(currentStream, "drain");
401
- } catch {
402
- break;
403
- }
404
- }
405
- }
406
- // Idle leftover heartbeats (after STREAM_DONE, before the next open) are ignored.
407
- }
408
- })();
409
- }