Spaces:
Runtime error
Runtime error
| /** | |
| * cursor-tap β capture cursor agent.v1.AgentService/Run wire bytes for tests. | |
| * | |
| * Usage: | |
| * CURSOR_TOKEN=... node scripts/ad-hoc/cursor-tap.cjs <fixture-name> <prompt> | |
| * | |
| * Examples: | |
| * node scripts/ad-hoc/cursor-tap.cjs single-turn-chat "say only PING" | |
| * node scripts/ad-hoc/cursor-tap.cjs system-prompt "be brief|hi" # split on first '|' | |
| * node scripts/ad-hoc/cursor-tap.cjs tool-call "weather in Paris" --tools=get_weather | |
| * node scripts/ad-hoc/cursor-tap.cjs composer-2-fast "hi" --model=composer-2-fast | |
| * | |
| * Writes the upstream response bytes to tests/fixtures/cursor/<fixture-name>.bin | |
| * and prints decoded summary to stdout. Use these fixtures in unit tests to | |
| * catch schema drift in cursor-agent's protobuf format. | |
| * | |
| * Note: this is a one-time / on-demand tool. The .bin output is gitignored | |
| * by default; commit fixtures explicitly when you want them in the test | |
| * baseline (tests/fixtures/cursor/.gitignore controls this). | |
| */ | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const http2 = require("http2"); | |
| const crypto = require("crypto"); | |
| const args = process.argv.slice(2); | |
| if (args.length < 2) { | |
| console.error( | |
| "Usage: cursor-tap.cjs <fixture-name> <prompt> [--model=...] [--tools=name1,name2]" | |
| ); | |
| process.exit(1); | |
| } | |
| const [fixtureName, prompt, ...flags] = args; | |
| const flagMap = Object.fromEntries( | |
| flags.map((f) => { | |
| const m = f.match(/^--([^=]+)=(.*)$/); | |
| return m ? [m[1], m[2]] : [f.replace(/^--/, ""), true]; | |
| }) | |
| ); | |
| const token = process.env.CURSOR_TOKEN; | |
| if (!token) { | |
| console.error("Set CURSOR_TOKEN environment variable."); | |
| process.exit(1); | |
| } | |
| const model = flagMap.model || "auto"; | |
| const conversationId = crypto.randomUUID(); | |
| const requestId = crypto.randomUUID(); | |
| const traceParent = `00-${crypto.randomBytes(16).toString("hex")}-${crypto.randomBytes(8).toString("hex")}-01`; | |
| // βββ Minimal protobuf encoder (mirrors open-sse/utils/cursorAgentProtobuf.ts) β | |
| function encodeVarint(n) { | |
| const out = []; | |
| let v = BigInt(n); | |
| while (v > 0x7fn) { | |
| out.push(Number(v & 0x7fn) | 0x80); | |
| v >>= 7n; | |
| } | |
| out.push(Number(v)); | |
| return Buffer.from(out); | |
| } | |
| function tag(field, wt) { | |
| return encodeVarint((field << 3) | wt); | |
| } | |
| function lenField(f, payload) { | |
| return Buffer.concat([tag(f, 2), encodeVarint(payload.length), payload]); | |
| } | |
| function strField(f, s) { | |
| return lenField(f, Buffer.from(s, "utf8")); | |
| } | |
| function varintField(f, n) { | |
| return Buffer.concat([tag(f, 0), encodeVarint(n)]); | |
| } | |
| function wrapConnectFrame(payload) { | |
| const header = Buffer.alloc(5); | |
| header[0] = 0; | |
| header.writeUInt32BE(payload.length, 1); | |
| return Buffer.concat([header, payload]); | |
| } | |
| // AgentRunRequest body | |
| const userMessage = lenField( | |
| 1, | |
| Buffer.concat([ | |
| strField(1, prompt), | |
| strField(2, crypto.randomUUID()), | |
| lenField(3, Buffer.alloc(0)), | |
| varintField(4, 1), | |
| ]) | |
| ); | |
| const userMessageAction = lenField(1, userMessage); | |
| const action = lenField(2, userMessageAction); | |
| const conversationState = lenField(1, Buffer.alloc(0)); | |
| const requestedModel = lenField(9, strField(1, model === "auto" ? "default" : model)); | |
| const arr = Buffer.concat([ | |
| conversationState, | |
| action, | |
| lenField(4, Buffer.alloc(0)), // mcp_tools | |
| strField(5, conversationId), | |
| requestedModel, | |
| varintField(12, 0), | |
| strField(16, conversationId), | |
| ]); | |
| const acm = lenField(1, arr); | |
| const body = wrapConnectFrame(acm); | |
| // βββ h2 request ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const cleanToken = token.includes("::") ? token.split("::")[1] : token; | |
| const client = http2.connect("https://agentn.global.api5.cursor.sh"); | |
| const collected = []; | |
| let responseStatus = 0; | |
| const req = client.request({ | |
| ":method": "POST", | |
| ":path": "/agent.v1.AgentService/Run", | |
| ":authority": "agentn.global.api5.cursor.sh", | |
| ":scheme": "https", | |
| authorization: `Bearer ${cleanToken}`, | |
| "backend-traceparent": traceParent, | |
| "connect-accept-encoding": "gzip,br", | |
| "connect-protocol-version": "1", | |
| "content-type": "application/connect+proto", | |
| traceparent: traceParent, | |
| "user-agent": "connect-es/1.6.1", | |
| "x-cursor-client-type": "cli", | |
| "x-cursor-client-version": "cli-2025.10.21-b2dfaef", | |
| "x-ghost-mode": "true", | |
| "x-original-request-id": requestId, | |
| "x-request-id": requestId, | |
| }); | |
| req.on("response", (h) => { | |
| responseStatus = Number(h[":status"]); | |
| }); | |
| req.on("data", (chunk) => { | |
| collected.push(Buffer.from(chunk)); | |
| }); | |
| req.on("end", () => { | |
| const raw = Buffer.concat(collected); | |
| const outDir = path.join(__dirname, "..", "..", "tests", "fixtures", "cursor"); | |
| fs.mkdirSync(outDir, { recursive: true }); | |
| const outFile = path.join(outDir, `${fixtureName}.bin`); | |
| fs.writeFileSync(outFile, raw); | |
| console.log(`[cursor-tap] status=${responseStatus} bytes=${raw.length} β ${outFile}`); | |
| client.close(); | |
| }); | |
| req.on("error", (err) => { | |
| console.error("[cursor-tap] req error:", err); | |
| process.exit(1); | |
| }); | |
| req.write(body); | |
| // NOTE: we never end the request; cursor closes the stream itself when the | |
| // turn is done. For tool-using captures, the script may need to write | |
| // follow-up frames before the stream closes β extend as needed. | |
| // Safety timeout: if cursor doesn't close in 60s, dump what we have. | |
| setTimeout(() => { | |
| console.warn("[cursor-tap] safety timeout; closing"); | |
| try { | |
| req.close(); | |
| client.close(); | |
| } catch {} | |
| const raw = Buffer.concat(collected); | |
| if (raw.length > 0) { | |
| const outDir = path.join(__dirname, "..", "..", "tests", "fixtures", "cursor"); | |
| fs.mkdirSync(outDir, { recursive: true }); | |
| fs.writeFileSync(path.join(outDir, `${fixtureName}.bin`), raw); | |
| } | |
| process.exit(0); | |
| }, 60_000); | |