omk-ai 0.99.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +2 -0
  3. package/dist/env-api-keys.d.ts.map +1 -1
  4. package/dist/env-api-keys.js +2 -0
  5. package/dist/env-api-keys.js.map +1 -1
  6. package/dist/image-models.generated.d.ts.map +1 -1
  7. package/dist/image-models.generated.js +4 -4
  8. package/dist/image-models.generated.js.map +1 -1
  9. package/dist/models.d.ts.map +1 -1
  10. package/dist/models.generated.d.ts +17797 -5300
  11. package/dist/models.generated.d.ts.map +1 -1
  12. package/dist/models.generated.js +19686 -10883
  13. package/dist/models.generated.js.map +1 -1
  14. package/dist/models.js +33 -1
  15. package/dist/models.js.map +1 -1
  16. package/dist/providers/anthropic.d.ts.map +1 -1
  17. package/dist/providers/anthropic.js +2 -1
  18. package/dist/providers/anthropic.js.map +1 -1
  19. package/dist/providers/cursor.d.ts +41 -0
  20. package/dist/providers/cursor.d.ts.map +1 -0
  21. package/dist/providers/cursor.js +731 -0
  22. package/dist/providers/cursor.js.map +1 -0
  23. package/dist/providers/devin-api.d.ts +8 -0
  24. package/dist/providers/devin-api.d.ts.map +1 -1
  25. package/dist/providers/devin-api.js +39 -2
  26. package/dist/providers/devin-api.js.map +1 -1
  27. package/dist/providers/devin-connect-stream.d.ts.map +1 -1
  28. package/dist/providers/devin-connect-stream.js +3 -2
  29. package/dist/providers/devin-connect-stream.js.map +1 -1
  30. package/dist/providers/devin-connect.d.ts.map +1 -1
  31. package/dist/providers/devin-connect.js +46 -1
  32. package/dist/providers/devin-connect.js.map +1 -1
  33. package/dist/providers/devin-protobuf.d.ts +4 -0
  34. package/dist/providers/devin-protobuf.d.ts.map +1 -1
  35. package/dist/providers/devin-protobuf.js +9 -0
  36. package/dist/providers/devin-protobuf.js.map +1 -1
  37. package/dist/providers/devin-request.d.ts.map +1 -1
  38. package/dist/providers/devin-request.js +6 -7
  39. package/dist/providers/devin-request.js.map +1 -1
  40. package/dist/providers/devin.d.ts.map +1 -1
  41. package/dist/providers/devin.js +11 -5
  42. package/dist/providers/devin.js.map +1 -1
  43. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  44. package/dist/providers/openai-codex-responses.js +1 -1
  45. package/dist/providers/openai-codex-responses.js.map +1 -1
  46. package/dist/providers/openai-completions-compat.d.ts.map +1 -1
  47. package/dist/providers/openai-completions-compat.js +2 -0
  48. package/dist/providers/openai-completions-compat.js.map +1 -1
  49. package/dist/providers/openai-completions.d.ts.map +1 -1
  50. package/dist/providers/openai-completions.js +7 -4
  51. package/dist/providers/openai-completions.js.map +1 -1
  52. package/dist/providers/register-builtins.d.ts +3 -0
  53. package/dist/providers/register-builtins.d.ts.map +1 -1
  54. package/dist/providers/register-builtins.js +11 -0
  55. package/dist/providers/register-builtins.js.map +1 -1
  56. package/dist/types.d.ts +10 -2
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js.map +1 -1
  59. package/dist/utils/retry.d.ts.map +1 -1
  60. package/dist/utils/retry.js +2 -0
  61. package/dist/utils/retry.js.map +1 -1
  62. package/package.json +1 -1
@@ -0,0 +1,731 @@
1
+ /**
2
+ * Cursor subscription adapter (`cursor-agent` API).
3
+ *
4
+ * Implements the `agent.v1.AgentService/Run` bidirectional RPC over HTTP/2 at
5
+ * https://api2.cursor.sh with Connect-framed protobuf, following the same
6
+ * reverse-engineered protocol notes as the Devin adapter (see DEVIN-NOTICE).
7
+ *
8
+ * Turn model: each `stream()` call is one `Run` with a `userMessageAction` (or a
9
+ * `resumeAction` when the last message is not a user turn). Conversation history
10
+ * is replayed through `conversationState.rootPromptMessagesJson` — SHA256-keyed
11
+ * JSON message blobs the server fetches back through the kv channel
12
+ * (`getBlobArgs` → `getBlobResult`). System prompts ride `requestContext.rules`
13
+ * in the exec handshake (`requestContextArgs` → `requestContextResult`).
14
+ *
15
+ * Scope: text and thinking deltas, token usage, blob/context/interaction
16
+ * handshakes, and graceful rejection of tool execution frames. OMK tools are
17
+ * intentionally not advertised in `requestContext.tools` — a frame the server
18
+ * sends for client-side execution is answered with the protocol's `throw`
19
+ * failure channel instead of being run unguarded inside the provider. Wiring
20
+ * exec frames through governed OMK tools is a coding-agent bridge feature, not
21
+ * provider scope.
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ import http2 from "node:http2";
25
+ import { getEnvApiKey } from "../env-api-keys.js";
26
+ import { calculateCost } from "../models.js";
27
+ import { AssistantMessageEventStream } from "../utils/event-stream.js";
28
+ import { field, ProtoMessage } from "./devin-protobuf.js";
29
+ export const CURSOR_API_URL = "https://api2.cursor.sh";
30
+ export const CURSOR_CLIENT_VERSION = "cli-2026.02.13-41ac335";
31
+ const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
32
+ const CONNECT_END_STREAM_FLAG = 0b00000010;
33
+ const HEARTBEAT_INTERVAL_MS = 5_000;
34
+ const MAX_FRAME_BYTES = 16 * 1024 * 1024;
35
+ const REJECTED_SUFFIX = "not implemented by this client";
36
+ const CURSOR_DEBUG = process.env.OMK_DEBUG_CURSOR === "1";
37
+ // ---------------------------------------------------------------------------
38
+ // Protobuf wire helpers (field numbers from the Cursor agent.v1 schema)
39
+ // ---------------------------------------------------------------------------
40
+ function frameConnectMessage(data, flags = 0) {
41
+ const frame = Buffer.alloc(5 + data.length);
42
+ frame[0] = flags;
43
+ frame.writeUInt32BE(data.length, 1);
44
+ frame.set(data, 5);
45
+ return frame;
46
+ }
47
+ function blobId(data) {
48
+ return new Uint8Array(createHash("sha256").update(data).digest());
49
+ }
50
+ /** Raw blob for pre-encoded payload bytes (system prompt JSON strings). */
51
+ function rawBlob(store, data) {
52
+ const id = blobId(data);
53
+ store.set(Buffer.from(id).toString("hex"), data);
54
+ return id;
55
+ }
56
+ /** JSON message blob for `rootPromptMessagesJson` (Vercel-AI-SDK-shaped). */
57
+ function jsonBlob(store, value) {
58
+ return rawBlob(store, new TextEncoder().encode(JSON.stringify(value)));
59
+ }
60
+ function readBlob(store, id) {
61
+ return store.get(Buffer.from(id).toString("hex"));
62
+ }
63
+ function normalizeToolCallId(id) {
64
+ const cleaned = id.replace(/[^a-zA-Z0-9_-]/g, "");
65
+ return cleaned || crypto.randomUUID();
66
+ }
67
+ function normalizeCursorArgs(value, seen = new Set()) {
68
+ if (value === null || typeof value === "string" || typeof value === "boolean")
69
+ return value;
70
+ if (typeof value === "number")
71
+ return Number.isFinite(value) ? value : null;
72
+ if (typeof value !== "object" || seen.has(value))
73
+ return undefined;
74
+ seen.add(value);
75
+ try {
76
+ if (Array.isArray(value))
77
+ return value.map((item) => normalizeCursorArgs(item, seen) ?? null);
78
+ const out = {};
79
+ for (const [key, item] of Object.entries(value)) {
80
+ const normalized = normalizeCursorArgs(item, seen);
81
+ if (normalized !== undefined)
82
+ out[key] = normalized;
83
+ }
84
+ return out;
85
+ }
86
+ finally {
87
+ seen.delete(value);
88
+ }
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Request building
92
+ // ---------------------------------------------------------------------------
93
+ const OPENAI_WIRE_PREFIX = /^(gpt-|composer|o\d)/i;
94
+ // Longest match first: `extra-high` must precede `high`, `xhigh` before `high`.
95
+ const EFFORT_SUFFIXES = ["extra-high", "minimal", "xhigh", "medium", "high", "low", "max", "none"];
96
+ function splitEffortSuffix(id) {
97
+ let rest = id;
98
+ let fast = false;
99
+ if (rest.endsWith("-fast")) {
100
+ fast = true;
101
+ rest = rest.slice(0, -5);
102
+ }
103
+ for (const suffix of EFFORT_SUFFIXES) {
104
+ if (rest.endsWith(`-${suffix}`)) {
105
+ return { base: rest.slice(0, -(suffix.length + 1)), effort: suffix, fast };
106
+ }
107
+ }
108
+ return { base: id, fast };
109
+ }
110
+ function isOpenAiFamily(id) {
111
+ return OPENAI_WIRE_PREFIX.test(id) && !id.startsWith("composer");
112
+ }
113
+ /**
114
+ * Resolve the wire `model_id` and effort `parameters`. Cursor's Run endpoint
115
+ * rejects effort sibling slugs for OpenAI-family ids (`resource_exhausted`);
116
+ * the official agent splits the slug into the base id plus a `reasoning`
117
+ * parameter. Non-OpenAI ids pass through whole. A bare `composer-2.5` pins
118
+ * `fast=false` because the server resolves it to the Fast tier otherwise.
119
+ */
120
+ export function resolveCursorWireModel(modelId) {
121
+ const { base, effort, fast } = splitEffortSuffix(modelId);
122
+ if (effort !== undefined && isOpenAiFamily(base)) {
123
+ if (effort === "none")
124
+ return { modelId: fast ? `${base}-fast` : base, parameters: [] };
125
+ // `extra-high` is the display suffix for the `xhigh` wire tier.
126
+ const wireEffort = effort === "extra-high" ? "xhigh" : effort;
127
+ return {
128
+ modelId: fast ? `${base}-fast` : base,
129
+ parameters: [Buffer.concat([field(1, "reasoning"), field(2, wireEffort)])],
130
+ };
131
+ }
132
+ if (modelId === "composer-2.5") {
133
+ return { modelId, parameters: [Buffer.concat([field(1, "fast"), field(2, "false")])] };
134
+ }
135
+ return { modelId, parameters: [] };
136
+ }
137
+ function systemPromptJsons(systemPrompt) {
138
+ const prompts = (systemPrompt ?? "")
139
+ .split(/\n{3,}/)
140
+ .map((part) => part.trim())
141
+ .filter(Boolean);
142
+ if (prompts.length === 0)
143
+ return [JSON.stringify({ role: "system", content: "You are a helpful assistant." })];
144
+ return prompts.map((content) => JSON.stringify({ role: "system", content }));
145
+ }
146
+ function rootPromptContent(content) {
147
+ const parts = [];
148
+ if (typeof content === "string") {
149
+ const text = content.trim();
150
+ if (text)
151
+ parts.push({ type: "text", text });
152
+ return parts;
153
+ }
154
+ for (const item of content) {
155
+ if (item.type === "text") {
156
+ const text = item.text.trim();
157
+ if (text)
158
+ parts.push({ type: "text", text });
159
+ }
160
+ else if (item.type === "image") {
161
+ const image = item;
162
+ parts.push({ type: "image", image: `data:${image.mimeType};base64,${image.data}`, mediaType: image.mimeType });
163
+ }
164
+ }
165
+ return parts;
166
+ }
167
+ function assistantContent(msg, targetModelId) {
168
+ const parts = [];
169
+ if (typeof msg.content === "string") {
170
+ if (msg.content.trim())
171
+ parts.push({ type: "text", text: msg.content });
172
+ return parts;
173
+ }
174
+ for (const item of msg.content) {
175
+ if (item.type === "text" && item.text) {
176
+ parts.push({ type: "text", text: item.text });
177
+ }
178
+ else if (item.type === "thinking" && item.thinking) {
179
+ // Replay reasoning only for blocks this provider produced for the same
180
+ // wire model; foreign-provider thinking goes out unsigned and can get
181
+ // the whole Run rejected.
182
+ const msgModel = msg.model;
183
+ if (msg.provider === "cursor" && msgModel === targetModelId) {
184
+ parts.push({
185
+ type: "reasoning",
186
+ text: item.thinking,
187
+ providerOptions: { cursor: { modelName: msgModel } },
188
+ ...(item.thinkingSignature ? { signature: item.thinkingSignature } : {}),
189
+ });
190
+ }
191
+ }
192
+ else if (item.type === "toolCall") {
193
+ parts.push({
194
+ type: "tool-call",
195
+ toolCallId: normalizeToolCallId(item.id),
196
+ toolName: item.name,
197
+ args: normalizeCursorArgs(item.arguments) ?? {},
198
+ });
199
+ }
200
+ }
201
+ return parts;
202
+ }
203
+ function toolResultText(result) {
204
+ return result.content
205
+ .map((item) => (item.type === "text" ? item.text : `[${item.mimeType} image]`))
206
+ .join("\n");
207
+ }
208
+ function buildRootPromptBlobIds(messages, systemPromptIds, blobStore, historyEnd, targetModelId) {
209
+ const ids = [...systemPromptIds];
210
+ const pairedToolCallIds = new Set();
211
+ for (let i = 0; i < historyEnd; i++) {
212
+ const msg = messages[i];
213
+ if (msg.role === "assistant") {
214
+ for (const item of Array.isArray(msg.content) ? msg.content : []) {
215
+ if (item.type === "toolCall")
216
+ pairedToolCallIds.add(item.id);
217
+ }
218
+ }
219
+ }
220
+ for (let i = 0; i < historyEnd && i < messages.length; i++) {
221
+ const msg = messages[i];
222
+ if (msg.role === "user") {
223
+ const content = rootPromptContent(msg.content);
224
+ if (content.length === 0)
225
+ continue;
226
+ ids.push(jsonBlob(blobStore, { role: "user", content }));
227
+ }
228
+ else if (msg.role === "assistant") {
229
+ const content = assistantContent(msg, targetModelId);
230
+ if (content.length === 0)
231
+ continue;
232
+ ids.push(jsonBlob(blobStore, { role: "assistant", content }));
233
+ }
234
+ else if (msg.role === "toolResult") {
235
+ const result = msg;
236
+ if (!pairedToolCallIds.has(result.toolCallId)) {
237
+ ids.push(jsonBlob(blobStore, {
238
+ role: "assistant",
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `${result.isError ? "[Tool Error]" : "[Tool Result]"}\n${toolResultText(result) || "(empty result)"}`,
243
+ },
244
+ ],
245
+ }));
246
+ continue;
247
+ }
248
+ const toolCallId = normalizeToolCallId(result.toolCallId);
249
+ ids.push(jsonBlob(blobStore, {
250
+ role: "tool",
251
+ id: toolCallId,
252
+ content: [
253
+ {
254
+ type: "tool-result",
255
+ toolName: result.toolName,
256
+ toolCallId,
257
+ result: toolResultText(result),
258
+ ...(result.isError ? { isError: true } : {}),
259
+ },
260
+ ],
261
+ }));
262
+ }
263
+ }
264
+ return ids;
265
+ }
266
+ function userMessageProto(content, text) {
267
+ const images = Array.isArray(content) ? content.filter((c) => c.type === "image") : [];
268
+ const fields = [field(1, text), field(2, crypto.randomUUID())];
269
+ if (images.length > 0) {
270
+ const selected = images.map((image) => Buffer.concat([
271
+ field(2, crypto.randomUUID()),
272
+ field(7, image.mimeType),
273
+ field(8, Buffer.from(image.data, "base64")),
274
+ ]));
275
+ fields.push(field(3, Buffer.concat(selected.map((img) => field(1, img)))));
276
+ }
277
+ return Buffer.concat(fields);
278
+ }
279
+ function cursorRule(index, content) {
280
+ return Buffer.concat([
281
+ field(1, `/omk/system-prompt/${index}.mdc`),
282
+ field(2, content),
283
+ field(3, field(1, new Uint8Array(0))), // type: global {}
284
+ field(4, 2), // source: USER
285
+ ]);
286
+ }
287
+ function buildRequestContext(systemPrompt) {
288
+ const rules = systemPromptJsons(systemPrompt).map((json, index) => {
289
+ const content = JSON.parse(json).content;
290
+ return field(2, cursorRule(index, content));
291
+ });
292
+ return Buffer.concat(rules);
293
+ }
294
+ function buildRunRequest(model, context, options) {
295
+ const blobStore = new Map();
296
+ const systemPromptIds = systemPromptJsons(context.systemPrompt).map((json) => rawBlob(blobStore, new TextEncoder().encode(json)));
297
+ const activeIndex = context.messages.length - 1;
298
+ const active = context.messages[activeIndex];
299
+ const activeUser = active && active.role === "user" ? active : undefined;
300
+ const userText = activeUser
301
+ ? typeof activeUser.content === "string"
302
+ ? activeUser.content.trim()
303
+ : activeUser.content
304
+ .filter((c) => c.type === "text")
305
+ .map((c) => c.text)
306
+ .join("\n")
307
+ .trim()
308
+ : "";
309
+ const hasImages = activeUser && Array.isArray(activeUser.content) ? activeUser.content.some((c) => c.type === "image") : false;
310
+ const historyEnd = activeUser ? activeIndex : context.messages.length;
311
+ const rootIds = buildRootPromptBlobIds(context.messages, systemPromptIds, blobStore, historyEnd, model.id);
312
+ const conversationState = Buffer.concat(rootIds.map((id) => field(1, id)));
313
+ const action = activeUser && (userText.length > 0 || hasImages)
314
+ ? field(1, field(1, userMessageProto(activeUser.content, userText)))
315
+ : field(2, new Uint8Array(0)); // resumeAction {}
316
+ const { modelId: wireModelId, parameters } = resolveCursorWireModel(model.id);
317
+ const maxMode = cursorMaxModeFor(model.id);
318
+ const modelDetails = Buffer.concat([
319
+ field(1, wireModelId),
320
+ field(3, model.id),
321
+ field(4, model.name),
322
+ ...(maxMode ? [field(7, true)] : []),
323
+ ]);
324
+ const requestedModel = Buffer.concat([
325
+ field(1, wireModelId),
326
+ ...(maxMode ? [field(2, true)] : []),
327
+ ...parameters.map((parameter) => field(3, parameter)),
328
+ ]);
329
+ const runRequest = Buffer.concat([
330
+ field(1, conversationState),
331
+ field(2, action),
332
+ field(3, modelDetails),
333
+ field(9, requestedModel),
334
+ field(5, options.sessionId ?? crypto.randomUUID()),
335
+ ]);
336
+ return { bytes: field(1, runRequest), blobStore };
337
+ }
338
+ /** Mirror the server-declared `maxMode` flag (fast Claude lanes). */
339
+ function cursorMaxModeFor(modelId) {
340
+ return /^claude-.*-fast$/.test(modelId);
341
+ }
342
+ // ---------------------------------------------------------------------------
343
+ // Outbound client messages
344
+ // ---------------------------------------------------------------------------
345
+ function execClientResult(id, execId, caseNo, result) {
346
+ const execMessage = Buffer.concat([field(1, id), field(15, execId), field(caseNo, result)]);
347
+ return frameConnectMessage(field(2, execMessage));
348
+ }
349
+ function execThrow(id, _execId, error) {
350
+ const throwMsg = field(5, Buffer.concat([field(2, Buffer.concat([field(1, id), field(2, error)]))]));
351
+ const closeMsg = field(5, field(1, field(1, id)));
352
+ return [frameConnectMessage(throwMsg), frameConnectMessage(closeMsg)];
353
+ }
354
+ function kvBlobResult(id, blobData) {
355
+ const kv = Buffer.concat([field(1, id), field(2, blobData ? field(1, blobData) : new Uint8Array(0))]);
356
+ return frameConnectMessage(field(3, kv));
357
+ }
358
+ function kvSetBlobResult(id) {
359
+ return frameConnectMessage(field(3, Buffer.concat([field(1, id), field(3, new Uint8Array(0))])));
360
+ }
361
+ function interactionResponse(id, fieldNo, payload) {
362
+ const response = Buffer.concat([field(1, id), field(fieldNo, payload)]);
363
+ return frameConnectMessage(field(6, response));
364
+ }
365
+ export const streamCursor = (model, context, options = {}) => {
366
+ const stream = new AssistantMessageEventStream();
367
+ const output = {
368
+ role: "assistant",
369
+ api: model.api,
370
+ provider: model.provider,
371
+ model: model.id,
372
+ content: [],
373
+ usage: {
374
+ input: 0,
375
+ output: 0,
376
+ cacheRead: 0,
377
+ cacheWrite: 0,
378
+ totalTokens: 0,
379
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
380
+ },
381
+ stopReason: "stop",
382
+ timestamp: Date.now(),
383
+ };
384
+ (async () => {
385
+ const controller = new AbortController();
386
+ const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
387
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 600_000);
388
+ let h2Client;
389
+ let h2Request;
390
+ let heartbeatTimer;
391
+ let sawTurnEnded = false;
392
+ let endStreamError;
393
+ const write = (frame) => {
394
+ if (frame && h2Request && !h2Request.closed && !h2Request.destroyed)
395
+ h2Request.write(frame);
396
+ };
397
+ try {
398
+ signal.throwIfAborted();
399
+ const token = (options.apiKey ?? getEnvApiKey("cursor") ?? "").trim();
400
+ if (!token)
401
+ throw new Error("Missing Cursor access token; run /login cursor");
402
+ const baseUrl = (model.baseUrl || CURSOR_API_URL).replace(/\/+$/, "");
403
+ if (!/^https:\/\/[a-z0-9.-]*cursor\.(sh|com|dev)(:\d+)?$/.test(baseUrl) &&
404
+ !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(baseUrl)) {
405
+ throw new Error("Cursor subscription credentials require a *.cursor.sh/cursor.com HTTPS origin");
406
+ }
407
+ const built = buildRunRequest(model, context, options);
408
+ const requestContext = buildRequestContext(context.systemPrompt);
409
+ const request = await options.onPayload?.(built.bytes, model);
410
+ const payload = request === undefined ? built.bytes : request;
411
+ if (!(payload instanceof Uint8Array) || new ProtoMessage(payload).messages(1).length !== 1)
412
+ throw new Error("Cursor payload hook must return protobuf bytes for a single runRequest");
413
+ h2Client = http2.connect(baseUrl);
414
+ h2Client.on("error", (error) => {
415
+ endStreamError ??= error instanceof Error ? error : new Error(String(error));
416
+ });
417
+ const requestHeaders = {
418
+ ":method": "POST",
419
+ ":path": CURSOR_RUN_PATH,
420
+ "content-type": "application/connect+proto",
421
+ "connect-protocol-version": "1",
422
+ te: "trailers",
423
+ authorization: `Bearer ${token}`,
424
+ "x-ghost-mode": "true",
425
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
426
+ "x-cursor-client-type": "cli",
427
+ "x-request-id": crypto.randomUUID(),
428
+ ...(options.headers ?? {}),
429
+ };
430
+ h2Request = h2Client.request(requestHeaders);
431
+ h2Request.on("response", (headers) => {
432
+ void options.onResponse?.({ status: Number(headers[":status"] ?? 0), headers: headers }, model);
433
+ });
434
+ let textBlock;
435
+ let thinkingBlock;
436
+ const endText = () => {
437
+ if (!textBlock)
438
+ return;
439
+ stream.push({ type: "text_end", contentIndex: textBlock.index, content: textBlock.text, partial: output });
440
+ textBlock = undefined;
441
+ };
442
+ const endThinking = () => {
443
+ if (!thinkingBlock)
444
+ return;
445
+ stream.push({
446
+ type: "thinking_end",
447
+ contentIndex: thinkingBlock.index,
448
+ content: thinkingBlock.text,
449
+ partial: output,
450
+ });
451
+ thinkingBlock = undefined;
452
+ };
453
+ stream.push({ type: "start", partial: output });
454
+ h2Request.write(frameConnectMessage(payload));
455
+ heartbeatTimer = setInterval(() => write(frameConnectMessage(field(7, new Uint8Array(0)))), HEARTBEAT_INTERVAL_MS);
456
+ if (options.signal) {
457
+ options.signal.addEventListener("abort", () => h2Request?.close(), { once: true });
458
+ }
459
+ await new Promise((resolve) => {
460
+ let pending = Buffer.alloc(0);
461
+ h2Request.on("data", (chunk) => {
462
+ pending = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
463
+ while (pending.length >= 5) {
464
+ const flags = pending[0];
465
+ const length = pending.readUInt32BE(1);
466
+ if (length > MAX_FRAME_BYTES) {
467
+ endStreamError ??= new Error(`Cursor frame exceeds ${MAX_FRAME_BYTES} bytes`);
468
+ h2Request?.close();
469
+ return;
470
+ }
471
+ if (pending.length < 5 + length)
472
+ break;
473
+ const messageBytes = pending.subarray(5, 5 + length);
474
+ pending = pending.subarray(5 + length);
475
+ if (flags & CONNECT_END_STREAM_FLAG) {
476
+ try {
477
+ const payload = JSON.parse(new TextDecoder().decode(messageBytes));
478
+ if (payload.error) {
479
+ endStreamError = new Error(`Cursor ${payload.error.code ?? "error"}: ${payload.error.message ?? "unknown error"}`);
480
+ }
481
+ }
482
+ catch {
483
+ endStreamError = new Error("Failed to parse Cursor end-stream trailer");
484
+ }
485
+ continue;
486
+ }
487
+ try {
488
+ dispatchServerMessage(new ProtoMessage(messageBytes));
489
+ }
490
+ catch (error) {
491
+ if (!endStreamError)
492
+ endStreamError = error instanceof Error ? error : new Error(String(error));
493
+ }
494
+ }
495
+ });
496
+ h2Request.on("trailers", (trailers) => {
497
+ const status = trailers["grpc-status"];
498
+ const message = trailers["grpc-message"];
499
+ if (status && status !== "0") {
500
+ endStreamError ??= new Error(`Cursor gRPC error ${status}: ${decodeURIComponent(String(message ?? ""))}`);
501
+ }
502
+ });
503
+ h2Request.on("end", resolve);
504
+ h2Request.on("error", (error) => {
505
+ endStreamError ??= error instanceof Error ? error : new Error(String(error));
506
+ resolve();
507
+ });
508
+ h2Request.on("close", resolve);
509
+ });
510
+ function dispatchServerMessage(message) {
511
+ if (CURSOR_DEBUG) {
512
+ const cases = [1, 2, 3, 4, 5, 7].filter((n) => message.has(n));
513
+ console.error(`[cursor] server cases=${cases.join(",")}`);
514
+ }
515
+ if (message.has(1)) {
516
+ for (const update of message.messages(1))
517
+ handleInteractionUpdate(update);
518
+ return;
519
+ }
520
+ if (message.has(2)) {
521
+ for (const exec of message.messages(2))
522
+ handleExecMessage(exec);
523
+ return;
524
+ }
525
+ if (message.has(4)) {
526
+ for (const kv of message.messages(4)) {
527
+ const id = kv.number(1);
528
+ for (const getBlob of kv.messages(2)) {
529
+ const blobIdBytes = getBlob.bytes(1) ?? new Uint8Array(0);
530
+ const data = readBlob(built.blobStore, blobIdBytes);
531
+ if (CURSOR_DEBUG)
532
+ console.error(`[cursor] kv getBlob id=${id} blob=${Buffer.from(blobIdBytes).toString("hex").slice(0, 16)} found=${!!data}`);
533
+ write(kvBlobResult(id, data));
534
+ }
535
+ for (const setBlob of kv.messages(3)) {
536
+ const blobIdBytes = setBlob.bytes(1);
537
+ const blobData = setBlob.bytes(2);
538
+ if (blobIdBytes && blobData) {
539
+ built.blobStore.set(Buffer.from(blobIdBytes).toString("hex"), blobData);
540
+ }
541
+ write(kvSetBlobResult(id));
542
+ if (CURSOR_DEBUG)
543
+ console.error(`[cursor] kv setBlob id=${id}`);
544
+ }
545
+ }
546
+ return;
547
+ }
548
+ if (message.has(5)) {
549
+ // execServerControlMessage: abort/stop signals — treat as turn end.
550
+ for (const control of message.messages(5)) {
551
+ if (control.has(1))
552
+ sawTurnEnded = true;
553
+ }
554
+ return;
555
+ }
556
+ if (message.has(7)) {
557
+ for (const query of message.messages(7))
558
+ handleInteractionQuery(query);
559
+ }
560
+ }
561
+ function handleInteractionUpdate(update) {
562
+ if (CURSOR_DEBUG) {
563
+ const cases = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].filter((n) => update.has(n));
564
+ console.error(`[cursor] update cases=${cases.join(",")}`);
565
+ }
566
+ for (const delta of update.messages(1)) {
567
+ endThinking();
568
+ const text = delta.string(1);
569
+ if (!textBlock) {
570
+ textBlock = { index: output.content.length, text: "" };
571
+ output.content.push({ type: "text", text: "" });
572
+ stream.push({ type: "text_start", contentIndex: textBlock.index, partial: output });
573
+ }
574
+ textBlock.text += text;
575
+ output.content[textBlock.index].text = textBlock.text;
576
+ stream.push({ type: "text_delta", contentIndex: textBlock.index, delta: text, partial: output });
577
+ }
578
+ for (const delta of update.messages(4)) {
579
+ endText();
580
+ const text = delta.string(1);
581
+ if (!thinkingBlock) {
582
+ thinkingBlock = { index: output.content.length, text: "" };
583
+ output.content.push({ type: "thinking", thinking: "", thinkingSignature: "" });
584
+ stream.push({ type: "thinking_start", contentIndex: thinkingBlock.index, partial: output });
585
+ }
586
+ thinkingBlock.text += text;
587
+ output.content[thinkingBlock.index].thinking = thinkingBlock.text;
588
+ stream.push({ type: "thinking_delta", contentIndex: thinkingBlock.index, delta: text, partial: output });
589
+ }
590
+ for (const tokens of update.messages(8)) {
591
+ output.usage.output += tokens.number(1);
592
+ }
593
+ if (update.has(14)) {
594
+ endThinking();
595
+ endText();
596
+ sawTurnEnded = true;
597
+ }
598
+ }
599
+ function handleExecMessage(exec) {
600
+ const id = exec.number(1);
601
+ const execId = exec.string(15);
602
+ if (CURSOR_DEBUG)
603
+ console.error(`[cursor] exec id=${id} execId=${execId} case=${execCaseName(exec)}`);
604
+ if (exec.has(10)) {
605
+ // requestContextArgs — the exec handshake carrying rules/tools.
606
+ const result = field(1, field(1, requestContext));
607
+ write(execClientResult(id, execId, 10, result));
608
+ return;
609
+ }
610
+ for (const frame of execThrow(id, execId, `Cursor ${execCaseName(exec)} execution is ${REJECTED_SUFFIX}`)) {
611
+ write(frame);
612
+ }
613
+ }
614
+ function handleInteractionQuery(query) {
615
+ const id = query.number(1);
616
+ const approved = field(1, new Uint8Array(0));
617
+ for (const fieldNo of [2, 5, 6, 9]) {
618
+ if (query.has(fieldNo)) {
619
+ write(interactionResponse(id, fieldNo, approved));
620
+ return;
621
+ }
622
+ }
623
+ if (query.has(3)) {
624
+ write(interactionResponse(id, 3, field(3, field(1, `Interactive questions are ${REJECTED_SUFFIX}`))));
625
+ return;
626
+ }
627
+ if (query.has(4)) {
628
+ write(interactionResponse(id, 4, field(2, field(1, `Mode switches are ${REJECTED_SUFFIX}`))));
629
+ return;
630
+ }
631
+ if (query.has(7)) {
632
+ // createPlan → result{ error{ error } }
633
+ write(interactionResponse(id, 7, field(1, field(2, field(1, `Plan files are ${REJECTED_SUFFIX}`)))));
634
+ return;
635
+ }
636
+ // setupVmEnvironmentArgs (8) is intentionally left unanswered rather
637
+ // than reporting a fake success.
638
+ }
639
+ }
640
+ catch (error) {
641
+ endStreamError ??= error instanceof Error ? error : new Error(String(error));
642
+ }
643
+ finally {
644
+ if (heartbeatTimer)
645
+ clearInterval(heartbeatTimer);
646
+ // `end()` (graceful half-close) flushes exec/throw frames still queued
647
+ // behind processed data; `close()` is reserved for aborts.
648
+ if (options.signal?.aborted)
649
+ h2Request?.close();
650
+ else
651
+ h2Request?.end();
652
+ h2Client?.close();
653
+ clearTimeout(timer);
654
+ }
655
+ // Finalize outside the try so close-flush always runs once.
656
+ try {
657
+ if (endStreamError)
658
+ throw endStreamError;
659
+ if (!sawTurnEnded) {
660
+ throw options.signal?.aborted
661
+ ? Object.assign(new Error("aborted"), { name: "AbortError" })
662
+ : new Error("Cursor stream ended before the turn completed");
663
+ }
664
+ output.stopReason = "stop";
665
+ output.usage.totalTokens =
666
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
667
+ calculateCost(model, output.usage);
668
+ stream.push({ type: "done", reason: "stop", message: output });
669
+ }
670
+ catch (error) {
671
+ output.stopReason = options.signal?.aborted ? "aborted" : "error";
672
+ output.errorMessage = error instanceof Error ? error.message : "Cursor request failed";
673
+ stream.push({ type: "error", reason: output.stopReason, error: output });
674
+ }
675
+ finally {
676
+ stream.end(output);
677
+ }
678
+ })();
679
+ return stream;
680
+ };
681
+ function execCaseName(exec) {
682
+ for (const [no, name] of EXEC_CASE_NAMES) {
683
+ if (exec.has(no))
684
+ return name;
685
+ }
686
+ return "exec";
687
+ }
688
+ const EXEC_CASE_NAMES = [
689
+ [2, "shell"],
690
+ [3, "write"],
691
+ [4, "delete"],
692
+ [5, "grep"],
693
+ [7, "read"],
694
+ [8, "ls"],
695
+ [9, "diagnostics"],
696
+ [10, "requestContext"],
697
+ [11, "mcp"],
698
+ [14, "shellStream"],
699
+ [16, "backgroundShellSpawn"],
700
+ [17, "listMcpResources"],
701
+ [18, "readMcpResource"],
702
+ [20, "fetch"],
703
+ [21, "recordScreen"],
704
+ [22, "computerUse"],
705
+ [23, "writeShellStdin"],
706
+ [29, "redactedRead"],
707
+ [36, "mcpState"],
708
+ [27, "executeHook"],
709
+ [28, "subagent"],
710
+ [30, "forceBackgroundShell"],
711
+ [31, "forceBackgroundSubagent"],
712
+ [37, "subagentAwait"],
713
+ [38, "smartModeClassifier"],
714
+ [40, "canvasDiagnostics"],
715
+ [41, "shellAllowlistPrecheck"],
716
+ [42, "mcpAllowlistPrecheck"],
717
+ [43, "webFetchAllowlistPrecheck"],
718
+ [44, "gitDiff"],
719
+ [45, "piRead"],
720
+ [46, "piBash"],
721
+ [47, "piEdit"],
722
+ [48, "piWrite"],
723
+ [49, "piGrep"],
724
+ [50, "piFind"],
725
+ [51, "piLs"],
726
+ [52, "conversationSearch"],
727
+ [54, "agentStoreConflict"],
728
+ [56, "miniSweAgentBash"],
729
+ ];
730
+ export const streamSimpleCursor = (model, context, options) => streamCursor(model, context, options);
731
+ //# sourceMappingURL=cursor.js.map