@bastani/atomic 0.8.28 → 0.8.29-alpha.2
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 +30 -0
- package/dist/builtin/cursor/CHANGELOG.md +27 -0
- package/dist/builtin/cursor/LICENSE +26 -0
- package/dist/builtin/cursor/README.md +22 -0
- package/dist/builtin/cursor/index.ts +9 -0
- package/dist/builtin/cursor/package.json +46 -0
- package/dist/builtin/cursor/src/auth.ts +352 -0
- package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
- package/dist/builtin/cursor/src/config.ts +123 -0
- package/dist/builtin/cursor/src/conversation-state.ts +135 -0
- package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
- package/dist/builtin/cursor/src/model-mapper.ts +270 -0
- package/dist/builtin/cursor/src/models.ts +54 -0
- package/dist/builtin/cursor/src/native-loader.ts +71 -0
- package/dist/builtin/cursor/src/proto/README.md +34 -0
- package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
- package/dist/builtin/cursor/src/provider.ts +301 -0
- package/dist/builtin/cursor/src/stream.ts +564 -0
- package/dist/builtin/cursor/src/transport.ts +791 -0
- package/dist/builtin/intercom/CHANGELOG.md +4 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
- package/dist/builtin/mcp/CHANGELOG.md +4 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +12 -0
- package/dist/builtin/subagents/README.md +7 -3
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
- package/dist/builtin/subagents/agents/debugger.md +3 -5
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
- package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
- package/dist/builtin/subagents/src/shared/types.ts +1 -0
- package/dist/builtin/subagents/src/shared/utils.ts +50 -10
- package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
- package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
- package/dist/builtin/web-access/CHANGELOG.md +5 -1
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/github-extract.ts +1 -1
- package/dist/builtin/web-access/package.json +3 -3
- package/dist/builtin/workflows/CHANGELOG.md +18 -0
- package/dist/builtin/workflows/README.md +19 -1
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
- package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
- package/dist/builtin/workflows/src/shared/types.ts +8 -4
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +6 -0
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +20 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +17 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +11 -9
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +55 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/project-trust.d.ts +1 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +3 -3
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +72 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/structured-output.d.ts +39 -0
- package/dist/core/tools/structured-output.d.ts.map +1 -0
- package/dist/core/tools/structured-output.js +141 -0
- package/dist/core/tools/structured-output.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +36 -14
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +16 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +158 -11
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +39 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/custom-provider.md +1 -0
- package/docs/extensions.md +2 -2
- package/docs/models.md +2 -0
- package/docs/packages.md +3 -1
- package/docs/providers.md +15 -0
- package/docs/sdk.md +61 -0
- package/docs/security.md +1 -1
- package/docs/subagents.md +21 -0
- package/docs/usage.md +2 -0
- package/docs/workflows.md +10 -7
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/structured-output.ts +22 -53
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +12 -9
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import type { Context, Model, Api, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
buildCursorRpcHeaders,
|
|
4
|
+
CURSOR_API_BASE_URL,
|
|
5
|
+
CURSOR_CLIENT_VERSION,
|
|
6
|
+
CURSOR_GET_USABLE_MODELS_PATH,
|
|
7
|
+
CURSOR_RUN_PATH,
|
|
8
|
+
readStringField,
|
|
9
|
+
sanitizeDiagnosticText,
|
|
10
|
+
parseJsonObject,
|
|
11
|
+
type JsonObject,
|
|
12
|
+
} from "./config.js";
|
|
13
|
+
import type { CursorUsableModel } from "./model-mapper.js";
|
|
14
|
+
import {
|
|
15
|
+
formatCursorH2NativeLoadFailure,
|
|
16
|
+
loadCursorH2NativeBinding,
|
|
17
|
+
type CursorH2NativeBinding,
|
|
18
|
+
type CursorH2NativeStream,
|
|
19
|
+
} from "./native-loader.js";
|
|
20
|
+
import { CursorProtobufProtocolCodec } from "./proto/protobuf-codec.js";
|
|
21
|
+
export { CursorProtobufProtocolCodec } from "./proto/protobuf-codec.js";
|
|
22
|
+
|
|
23
|
+
export type CursorTransportErrorCode = "Unauthorized" | "CursorApiRejected" | "Aborted" | "NetworkError" | "ProtocolError";
|
|
24
|
+
|
|
25
|
+
export class CursorTransportError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly code: CursorTransportErrorCode,
|
|
28
|
+
message: string,
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "CursorTransportError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CursorTransportLifecycleSnapshot {
|
|
36
|
+
readonly openStreams: number;
|
|
37
|
+
readonly cancelledStreams: number;
|
|
38
|
+
readonly closedStreams: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CursorRunRequest {
|
|
42
|
+
readonly accessToken: string;
|
|
43
|
+
readonly requestId: string;
|
|
44
|
+
readonly conversationId?: string;
|
|
45
|
+
readonly model: Model<Api>;
|
|
46
|
+
readonly resolvedModelId: string;
|
|
47
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
48
|
+
readonly context: Context;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
50
|
+
readonly openTimeoutMs?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type CursorDoneReason = "stop" | "length" | "toolUse";
|
|
54
|
+
|
|
55
|
+
export interface CursorToolCallMessage {
|
|
56
|
+
readonly type: "toolCall";
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly argumentsJson: string;
|
|
60
|
+
readonly execId?: string;
|
|
61
|
+
readonly execNumericId?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type CursorServerMessage =
|
|
65
|
+
| { readonly type: "textDelta"; readonly text: string }
|
|
66
|
+
| { readonly type: "thinkingDelta"; readonly text: string }
|
|
67
|
+
| CursorToolCallMessage
|
|
68
|
+
| { readonly type: "usage"; readonly kind?: "checkpoint"; readonly inputTokens?: number; readonly outputTokens?: number; readonly cacheReadTokens?: number; readonly cacheWriteTokens?: number; readonly usedTokens?: number }
|
|
69
|
+
| { readonly type: "usage"; readonly kind: "outputDelta"; readonly outputTokens: number }
|
|
70
|
+
| { readonly type: "nonMcpExec"; readonly fieldNumber: number; readonly execId?: string; readonly execNumericId?: number }
|
|
71
|
+
| { readonly type: "done"; readonly reason: CursorDoneReason };
|
|
72
|
+
|
|
73
|
+
export type CursorControlMessage =
|
|
74
|
+
| { readonly type: "kvGetBlob"; readonly id: number; readonly blobId: Uint8Array }
|
|
75
|
+
| { readonly type: "kvSetBlob"; readonly id: number; readonly blobId: Uint8Array; readonly blobData: Uint8Array }
|
|
76
|
+
| { readonly type: "conversationCheckpoint"; readonly checkpoint: Uint8Array }
|
|
77
|
+
| { readonly type: "requestContext"; readonly execNumericId?: number; readonly execId?: string };
|
|
78
|
+
|
|
79
|
+
export type CursorProtocolMessage = CursorServerMessage | CursorControlMessage;
|
|
80
|
+
|
|
81
|
+
export interface CursorToolResultMessage {
|
|
82
|
+
readonly toolCallId: string;
|
|
83
|
+
readonly toolName: string;
|
|
84
|
+
readonly text: string;
|
|
85
|
+
readonly isError: boolean;
|
|
86
|
+
readonly execId?: string;
|
|
87
|
+
readonly execNumericId?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CursorWriteOptions {
|
|
91
|
+
readonly signal?: AbortSignal;
|
|
92
|
+
readonly timeoutMs?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CursorRunStream {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
readonly messages: AsyncIterable<CursorServerMessage>;
|
|
98
|
+
writeToolResult(result: CursorToolResultMessage, options?: CursorWriteOptions): Promise<void>;
|
|
99
|
+
cancel(): Promise<void>;
|
|
100
|
+
close(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface CursorAgentTransport {
|
|
104
|
+
getUsableModels(accessToken: string, requestId: string, signal?: AbortSignal): Promise<readonly CursorUsableModel[]>;
|
|
105
|
+
run(request: CursorRunRequest): Promise<CursorRunStream>;
|
|
106
|
+
dispose(): Promise<void>;
|
|
107
|
+
discardConversation?(conversationId: string): void;
|
|
108
|
+
getLifecycleSnapshot(): CursorTransportLifecycleSnapshot;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CursorConnectFrame {
|
|
112
|
+
readonly flags: number;
|
|
113
|
+
readonly data: Uint8Array;
|
|
114
|
+
readonly endStream: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CursorHttp2UnaryResponse {
|
|
118
|
+
readonly statusCode?: number;
|
|
119
|
+
readonly body: Uint8Array;
|
|
120
|
+
readonly headers: Record<string, string>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CursorHttp2StreamHandle {
|
|
124
|
+
readonly frames: AsyncIterable<Uint8Array>;
|
|
125
|
+
write(data: Uint8Array, options?: CursorWriteOptions): Promise<void>;
|
|
126
|
+
close(): Promise<void>;
|
|
127
|
+
cancel(): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface CursorHttp2Client {
|
|
131
|
+
requestUnary(request: {
|
|
132
|
+
readonly baseUrl: string;
|
|
133
|
+
readonly path: string;
|
|
134
|
+
readonly headers: Record<string, string>;
|
|
135
|
+
readonly body: Uint8Array;
|
|
136
|
+
readonly signal?: AbortSignal;
|
|
137
|
+
readonly timeoutMs?: number;
|
|
138
|
+
}): Promise<CursorHttp2UnaryResponse>;
|
|
139
|
+
openStream(request: {
|
|
140
|
+
readonly baseUrl: string;
|
|
141
|
+
readonly path: string;
|
|
142
|
+
readonly headers: Record<string, string>;
|
|
143
|
+
readonly signal?: AbortSignal;
|
|
144
|
+
readonly initialBody?: Uint8Array;
|
|
145
|
+
readonly timeoutMs?: number;
|
|
146
|
+
}): Promise<CursorHttp2StreamHandle>;
|
|
147
|
+
dispose(): Promise<void>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface CursorProtocolCodec {
|
|
151
|
+
encodeGetUsableModelsRequest(): Uint8Array;
|
|
152
|
+
decodeGetUsableModelsResponse(data: Uint8Array): readonly CursorUsableModel[];
|
|
153
|
+
encodeRunRequest(request: CursorRunRequest): Uint8Array;
|
|
154
|
+
decodeRunFrame(frame: CursorConnectFrame): readonly CursorProtocolMessage[];
|
|
155
|
+
encodeToolResult(result: CursorToolResultMessage): Uint8Array;
|
|
156
|
+
encodeCancelRequest(): Uint8Array;
|
|
157
|
+
encodeHeartbeatRequest(): Uint8Array;
|
|
158
|
+
encodeServerResponse?(message: CursorProtocolMessage, requestId: string): Uint8Array | undefined;
|
|
159
|
+
disposeRun?(requestId: string): void;
|
|
160
|
+
discardRun?(requestId: string): void;
|
|
161
|
+
discardConversation?(conversationId: string): void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Http2CursorAgentTransportOptions {
|
|
165
|
+
readonly baseUrl?: string;
|
|
166
|
+
readonly client?: CursorHttp2Client;
|
|
167
|
+
readonly codec?: CursorProtocolCodec;
|
|
168
|
+
readonly requestTimeoutMs?: number;
|
|
169
|
+
readonly streamOpenTimeoutMs?: number;
|
|
170
|
+
readonly heartbeatIntervalMs?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const CONNECT_END_STREAM_FLAG = 0b10;
|
|
174
|
+
const DEFAULT_CANCEL_WRITE_TIMEOUT_MS = 1_000;
|
|
175
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
176
|
+
|
|
177
|
+
export function encodeCursorConnectFrame(data: Uint8Array, flags = 0): Uint8Array {
|
|
178
|
+
const frame = new Uint8Array(5 + data.length);
|
|
179
|
+
frame[0] = flags;
|
|
180
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
181
|
+
view.setUint32(1, data.length, false);
|
|
182
|
+
frame.set(data, 5);
|
|
183
|
+
return frame;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function decodeCursorConnectFrames(data: Uint8Array): readonly CursorConnectFrame[] {
|
|
187
|
+
const decoder = new CursorConnectFrameDecoder();
|
|
188
|
+
const frames = decoder.push(data);
|
|
189
|
+
decoder.finish();
|
|
190
|
+
return frames;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class CursorConnectFrameDecoder {
|
|
194
|
+
#buffer: Uint8Array<ArrayBufferLike> = new Uint8Array();
|
|
195
|
+
|
|
196
|
+
push(data: Uint8Array): readonly CursorConnectFrame[] {
|
|
197
|
+
this.#buffer = concatBytes(this.#buffer, data);
|
|
198
|
+
const frames: CursorConnectFrame[] = [];
|
|
199
|
+
let offset = 0;
|
|
200
|
+
while (this.#buffer.length - offset >= 5) {
|
|
201
|
+
const flags = this.#buffer[offset] ?? 0;
|
|
202
|
+
const view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset + offset, this.#buffer.byteLength - offset);
|
|
203
|
+
const length = view.getUint32(1, false);
|
|
204
|
+
const bodyStart = offset + 5;
|
|
205
|
+
const bodyEnd = bodyStart + length;
|
|
206
|
+
if (bodyEnd > this.#buffer.length) break;
|
|
207
|
+
frames.push({ flags, data: this.#buffer.slice(bodyStart, bodyEnd), endStream: (flags & CONNECT_END_STREAM_FLAG) !== 0 });
|
|
208
|
+
offset = bodyEnd;
|
|
209
|
+
}
|
|
210
|
+
this.#buffer = this.#buffer.slice(offset);
|
|
211
|
+
return frames;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
finish(): void {
|
|
215
|
+
if (this.#buffer.length === 0) return;
|
|
216
|
+
if (this.#buffer.length < 5) throw new Error("Incomplete Cursor Connect frame header.");
|
|
217
|
+
throw new Error("Incomplete Cursor Connect frame body.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isCursorControlMessage(message: CursorProtocolMessage): message is CursorControlMessage {
|
|
222
|
+
return message.type === "kvGetBlob" || message.type === "kvSetBlob" || message.type === "conversationCheckpoint" || message.type === "requestContext";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async function runWithDeadline<T>(operation: (signal: AbortSignal | undefined) => Promise<T>, timeoutMs: number, parentSignal: AbortSignal | undefined, timeoutMessage: string): Promise<T> {
|
|
227
|
+
if (parentSignal?.aborted) throw new CursorTransportError("Aborted", "Cursor request aborted.");
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
let rejectAbort: ((error: CursorTransportError) => void) | undefined;
|
|
230
|
+
const onAbort = (): void => {
|
|
231
|
+
controller.abort();
|
|
232
|
+
rejectAbort?.(new CursorTransportError("Aborted", "Cursor request aborted."));
|
|
233
|
+
};
|
|
234
|
+
parentSignal?.addEventListener("abort", onAbort, { once: true });
|
|
235
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
236
|
+
const abortPromise = parentSignal ? new Promise<never>((_resolve, reject) => {
|
|
237
|
+
rejectAbort = reject;
|
|
238
|
+
}) : undefined;
|
|
239
|
+
const timeoutPromise = timeoutMs > 0 ? new Promise<never>((_resolve, reject) => {
|
|
240
|
+
timeout = setTimeout(() => {
|
|
241
|
+
controller.abort();
|
|
242
|
+
reject(new CursorTransportError("NetworkError", timeoutMessage));
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
timeout.unref?.();
|
|
245
|
+
}) : undefined;
|
|
246
|
+
try {
|
|
247
|
+
return await Promise.race([operation(controller.signal), ...(abortPromise ? [abortPromise] : []), ...(timeoutPromise ? [timeoutPromise] : [])]);
|
|
248
|
+
} finally {
|
|
249
|
+
if (timeout) clearTimeout(timeout);
|
|
250
|
+
parentSignal?.removeEventListener("abort", onAbort);
|
|
251
|
+
rejectAbort = undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let nativeOperationCounter = 0;
|
|
256
|
+
|
|
257
|
+
function nextNativeOperationId(): string {
|
|
258
|
+
nativeOperationCounter = (nativeOperationCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
259
|
+
return `cursor-h2-${Date.now().toString(36)}-${nativeOperationCounter.toString(36)}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number | undefined, timeoutMessage: string): Promise<T> {
|
|
263
|
+
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
264
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
265
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
266
|
+
timeout = setTimeout(() => reject(new CursorTransportError("NetworkError", timeoutMessage)), timeoutMs);
|
|
267
|
+
timeout.unref?.();
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
271
|
+
} finally {
|
|
272
|
+
if (timeout) clearTimeout(timeout);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined, message: string, onLateResolve?: (value: T) => void | Promise<void>, onAbort?: () => void | Promise<void>): Promise<T> {
|
|
277
|
+
if (signal?.aborted) throw new CursorTransportError("Aborted", message);
|
|
278
|
+
if (!signal) return promise;
|
|
279
|
+
let settled = false;
|
|
280
|
+
let rejectAbort: ((error: CursorTransportError) => void) | undefined;
|
|
281
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
282
|
+
rejectAbort = reject;
|
|
283
|
+
});
|
|
284
|
+
const abort = (): void => {
|
|
285
|
+
void onAbort?.();
|
|
286
|
+
rejectAbort?.(new CursorTransportError("Aborted", message));
|
|
287
|
+
};
|
|
288
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
289
|
+
try {
|
|
290
|
+
return await Promise.race([
|
|
291
|
+
promise.then(async (value) => {
|
|
292
|
+
settled = true;
|
|
293
|
+
if (signal.aborted) {
|
|
294
|
+
if (onLateResolve) await onLateResolve(value);
|
|
295
|
+
throw new CursorTransportError("Aborted", message);
|
|
296
|
+
}
|
|
297
|
+
return value;
|
|
298
|
+
}),
|
|
299
|
+
abortPromise,
|
|
300
|
+
]);
|
|
301
|
+
} finally {
|
|
302
|
+
signal.removeEventListener("abort", abort);
|
|
303
|
+
rejectAbort = undefined;
|
|
304
|
+
if (!settled) {
|
|
305
|
+
promise.then((value) => {
|
|
306
|
+
if (signal.aborted) void onLateResolve?.(value);
|
|
307
|
+
}).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export class Http2CursorAgentTransport implements CursorAgentTransport {
|
|
313
|
+
readonly #baseUrl: string;
|
|
314
|
+
readonly #client: CursorHttp2Client;
|
|
315
|
+
readonly #codec: CursorProtocolCodec;
|
|
316
|
+
readonly #requestTimeoutMs: number;
|
|
317
|
+
readonly #streamOpenTimeoutMs: number;
|
|
318
|
+
readonly #heartbeatIntervalMs: number;
|
|
319
|
+
#openStreams = 0;
|
|
320
|
+
#cancelledStreams = 0;
|
|
321
|
+
#closedStreams = 0;
|
|
322
|
+
|
|
323
|
+
constructor(baseUrlOrOptions: string | Http2CursorAgentTransportOptions = CURSOR_API_BASE_URL) {
|
|
324
|
+
const options = typeof baseUrlOrOptions === "string" ? { baseUrl: baseUrlOrOptions } : baseUrlOrOptions;
|
|
325
|
+
this.#baseUrl = options.baseUrl ?? CURSOR_API_BASE_URL;
|
|
326
|
+
this.#client = options.client ?? createDefaultCursorHttp2Client();
|
|
327
|
+
this.#codec = options.codec ?? new CursorProtobufProtocolCodec();
|
|
328
|
+
this.#requestTimeoutMs = options.requestTimeoutMs ?? 60_000;
|
|
329
|
+
this.#streamOpenTimeoutMs = options.streamOpenTimeoutMs ?? 60_000;
|
|
330
|
+
this.#heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async getUsableModels(accessToken: string, requestId: string, signal?: AbortSignal): Promise<readonly CursorUsableModel[]> {
|
|
334
|
+
if (signal?.aborted) {
|
|
335
|
+
throw new CursorTransportError("Aborted", "Cursor model discovery was aborted before the request started.");
|
|
336
|
+
}
|
|
337
|
+
const headers = buildCursorRpcHeaders(accessToken, requestId, "application/proto");
|
|
338
|
+
try {
|
|
339
|
+
const response = await runWithDeadline(
|
|
340
|
+
(parentSignal) => this.#client.requestUnary({
|
|
341
|
+
baseUrl: this.#baseUrl,
|
|
342
|
+
path: CURSOR_GET_USABLE_MODELS_PATH,
|
|
343
|
+
headers,
|
|
344
|
+
body: this.#codec.encodeGetUsableModelsRequest(),
|
|
345
|
+
signal: parentSignal,
|
|
346
|
+
timeoutMs: this.#requestTimeoutMs,
|
|
347
|
+
}),
|
|
348
|
+
this.#requestTimeoutMs,
|
|
349
|
+
signal,
|
|
350
|
+
"Cursor model discovery timed out.",
|
|
351
|
+
);
|
|
352
|
+
assertSuccessfulStatus(response.statusCode, response.body, [accessToken]);
|
|
353
|
+
// GetUsableModels uses application/proto unary bodies, not Connect
|
|
354
|
+
// stream envelopes; pass the raw protobuf response to the codec.
|
|
355
|
+
return this.#codec.decodeGetUsableModelsResponse(response.body);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw sanitizeCursorTransportError(toError(error), [accessToken]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async run(request: CursorRunRequest): Promise<CursorRunStream> {
|
|
362
|
+
if (request.signal?.aborted) {
|
|
363
|
+
throw new CursorTransportError("Aborted", "Cursor stream was aborted before the request started.");
|
|
364
|
+
}
|
|
365
|
+
const headers = {
|
|
366
|
+
...buildCursorRpcHeaders(request.accessToken, request.requestId, "application/connect+proto"),
|
|
367
|
+
"connect-protocol-version": "1",
|
|
368
|
+
};
|
|
369
|
+
try {
|
|
370
|
+
const initialBody = encodeCursorConnectFrame(this.#codec.encodeRunRequest(request));
|
|
371
|
+
const handle = await runWithDeadline(
|
|
372
|
+
(parentSignal) => this.#client.openStream({ baseUrl: this.#baseUrl, path: CURSOR_RUN_PATH, headers, signal: parentSignal, initialBody, timeoutMs: request.openTimeoutMs ?? this.#streamOpenTimeoutMs }),
|
|
373
|
+
request.openTimeoutMs ?? this.#streamOpenTimeoutMs,
|
|
374
|
+
request.signal,
|
|
375
|
+
"Cursor stream open timed out.",
|
|
376
|
+
);
|
|
377
|
+
this.#openStreams += 1;
|
|
378
|
+
return new Http2CursorRunStream(
|
|
379
|
+
request.requestId,
|
|
380
|
+
handle,
|
|
381
|
+
this.#codec,
|
|
382
|
+
[request.accessToken],
|
|
383
|
+
this.#heartbeatIntervalMs,
|
|
384
|
+
() => {
|
|
385
|
+
this.#cancelledStreams += 1;
|
|
386
|
+
},
|
|
387
|
+
() => {
|
|
388
|
+
this.#closedStreams += 1;
|
|
389
|
+
this.#openStreams = Math.max(0, this.#openStreams - 1);
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw sanitizeCursorTransportError(toError(error), [request.accessToken]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async dispose(): Promise<void> {
|
|
398
|
+
await this.#client.dispose();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
discardConversation(conversationId: string): void {
|
|
402
|
+
this.#codec.discardConversation?.(conversationId);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getLifecycleSnapshot(): CursorTransportLifecycleSnapshot {
|
|
406
|
+
return { openStreams: this.#openStreams, cancelledStreams: this.#cancelledStreams, closedStreams: this.#closedStreams };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
class Http2CursorRunStream implements CursorRunStream {
|
|
411
|
+
readonly messages: AsyncIterable<CursorServerMessage>;
|
|
412
|
+
#closed = false;
|
|
413
|
+
#cancelled = false;
|
|
414
|
+
readonly #heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
415
|
+
readonly #messageQueue: CursorServerMessage[] = [];
|
|
416
|
+
readonly #messageReaders: Array<{
|
|
417
|
+
readonly resolve: (value: IteratorResult<CursorServerMessage>) => void;
|
|
418
|
+
readonly reject: (error: unknown) => void;
|
|
419
|
+
}> = [];
|
|
420
|
+
#messageQueueFinished = false;
|
|
421
|
+
#messageQueueError: Error | undefined;
|
|
422
|
+
|
|
423
|
+
constructor(
|
|
424
|
+
readonly id: string,
|
|
425
|
+
readonly handle: CursorHttp2StreamHandle,
|
|
426
|
+
readonly codec: CursorProtocolCodec,
|
|
427
|
+
readonly secrets: readonly string[],
|
|
428
|
+
heartbeatIntervalMs: number,
|
|
429
|
+
readonly onCancel: () => void,
|
|
430
|
+
readonly onClose: () => void,
|
|
431
|
+
) {
|
|
432
|
+
this.messages = this.createMessages();
|
|
433
|
+
void this.pumpMessages();
|
|
434
|
+
if (heartbeatIntervalMs > 0) {
|
|
435
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
436
|
+
this.handle.write(encodeCursorConnectFrame(this.codec.encodeHeartbeatRequest())).catch(() => {
|
|
437
|
+
this.cancel().catch(() => undefined);
|
|
438
|
+
});
|
|
439
|
+
}, heartbeatIntervalMs);
|
|
440
|
+
this.#heartbeatTimer.unref?.();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async writeToolResult(result: CursorToolResultMessage, options?: CursorWriteOptions): Promise<void> {
|
|
445
|
+
if (this.#closed) throw new CursorTransportError("ProtocolError", "Cannot write Cursor tool result to a closed stream.");
|
|
446
|
+
try {
|
|
447
|
+
await this.handle.write(encodeCursorConnectFrame(this.codec.encodeToolResult(result)), options);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
await this.cancel().catch(() => undefined);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async cancel(): Promise<void> {
|
|
455
|
+
if (this.#cancelled) return;
|
|
456
|
+
this.#cancelled = true;
|
|
457
|
+
this.clearHeartbeat();
|
|
458
|
+
let cancelError: Error | undefined;
|
|
459
|
+
try {
|
|
460
|
+
await this.handle.write(encodeCursorConnectFrame(this.codec.encodeCancelRequest()), { timeoutMs: DEFAULT_CANCEL_WRITE_TIMEOUT_MS }).catch(() => undefined);
|
|
461
|
+
} finally {
|
|
462
|
+
this.onCancel();
|
|
463
|
+
try {
|
|
464
|
+
await this.handle.cancel();
|
|
465
|
+
} catch (error) {
|
|
466
|
+
cancelError = toError(error);
|
|
467
|
+
} finally {
|
|
468
|
+
this.finishMessageQueue();
|
|
469
|
+
if (!this.#closed) {
|
|
470
|
+
this.#closed = true;
|
|
471
|
+
this.codec.disposeRun?.(this.id);
|
|
472
|
+
this.onClose();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (cancelError) throw cancelError;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async close(): Promise<void> {
|
|
480
|
+
if (this.#closed) return;
|
|
481
|
+
this.#closed = true;
|
|
482
|
+
this.clearHeartbeat();
|
|
483
|
+
try {
|
|
484
|
+
await this.handle.close();
|
|
485
|
+
} finally {
|
|
486
|
+
this.finishMessageQueue();
|
|
487
|
+
this.codec.disposeRun?.(this.id);
|
|
488
|
+
this.onClose();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private clearHeartbeat(): void {
|
|
493
|
+
if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private async pumpMessages(): Promise<void> {
|
|
497
|
+
const decoder = new CursorConnectFrameDecoder();
|
|
498
|
+
try {
|
|
499
|
+
for await (const raw of this.handle.frames) {
|
|
500
|
+
if (this.#closed || this.#cancelled) break;
|
|
501
|
+
for (const frame of decoder.push(raw)) {
|
|
502
|
+
if (this.#closed || this.#cancelled) break;
|
|
503
|
+
if (frame.endStream) {
|
|
504
|
+
try {
|
|
505
|
+
throwIfCursorEndStreamError(frame.data, this.secrets);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
this.codec.discardRun?.(this.id);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
for (const message of this.codec.decodeRunFrame(frame)) {
|
|
513
|
+
if (this.#closed || this.#cancelled) break;
|
|
514
|
+
const response = this.codec.encodeServerResponse?.(message, this.id);
|
|
515
|
+
if (response) {
|
|
516
|
+
await this.handle.write(encodeCursorConnectFrame(response));
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!isCursorControlMessage(message)) this.enqueueMessage(message);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
decoder.finish();
|
|
524
|
+
this.finishMessageQueue();
|
|
525
|
+
} catch (error) {
|
|
526
|
+
this.finishMessageQueue(toError(error));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private enqueueMessage(message: CursorServerMessage): void {
|
|
531
|
+
if (this.#messageQueueFinished) return;
|
|
532
|
+
this.#messageQueue.push(message);
|
|
533
|
+
this.flushMessageReaders();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private finishMessageQueue(error?: Error): void {
|
|
537
|
+
if (this.#messageQueueFinished) return;
|
|
538
|
+
this.#messageQueueFinished = true;
|
|
539
|
+
this.#messageQueueError = error;
|
|
540
|
+
this.flushMessageReaders();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private flushMessageReaders(): void {
|
|
544
|
+
while (this.#messageReaders.length > 0) {
|
|
545
|
+
const reader = this.#messageReaders.shift();
|
|
546
|
+
if (!reader) return;
|
|
547
|
+
const message = this.#messageQueue.shift();
|
|
548
|
+
if (message !== undefined) {
|
|
549
|
+
reader.resolve({ value: message, done: false });
|
|
550
|
+
} else if (this.#messageQueueError) {
|
|
551
|
+
reader.reject(this.#messageQueueError);
|
|
552
|
+
} else if (this.#messageQueueFinished) {
|
|
553
|
+
reader.resolve({ value: undefined, done: true });
|
|
554
|
+
} else {
|
|
555
|
+
this.#messageReaders.unshift(reader);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private nextMessage(): Promise<IteratorResult<CursorServerMessage>> {
|
|
562
|
+
const message = this.#messageQueue.shift();
|
|
563
|
+
if (message !== undefined) return Promise.resolve({ value: message, done: false });
|
|
564
|
+
if (this.#messageQueueError) return Promise.reject(this.#messageQueueError);
|
|
565
|
+
if (this.#messageQueueFinished) return Promise.resolve({ value: undefined, done: true });
|
|
566
|
+
return new Promise((resolve, reject) => {
|
|
567
|
+
this.#messageReaders.push({ resolve, reject });
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async *createMessages(): AsyncIterable<CursorServerMessage> {
|
|
572
|
+
while (true) {
|
|
573
|
+
const next = await this.nextMessage();
|
|
574
|
+
if (next.done) return;
|
|
575
|
+
yield next.value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function createDefaultCursorHttp2Client(): CursorHttp2Client {
|
|
581
|
+
return new LazyNativeHttp2CursorClient();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
class LazyNativeHttp2CursorClient implements CursorHttp2Client {
|
|
585
|
+
#client: NativeHttp2CursorClient | undefined;
|
|
586
|
+
|
|
587
|
+
private get client(): NativeHttp2CursorClient {
|
|
588
|
+
if (this.#client) return this.#client;
|
|
589
|
+
const native = loadCursorH2NativeBinding();
|
|
590
|
+
if (!native.ok) throw new CursorTransportError("NetworkError", formatCursorH2NativeLoadFailure(native));
|
|
591
|
+
this.#client = new NativeHttp2CursorClient(native.binding);
|
|
592
|
+
return this.#client;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async requestUnary(request: { readonly baseUrl: string; readonly path: string; readonly headers: Record<string, string>; readonly body: Uint8Array; readonly signal?: AbortSignal; readonly timeoutMs?: number }): Promise<CursorHttp2UnaryResponse> {
|
|
596
|
+
return this.client.requestUnary(request);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async openStream(request: { readonly baseUrl: string; readonly path: string; readonly headers: Record<string, string>; readonly signal?: AbortSignal; readonly initialBody?: Uint8Array; readonly timeoutMs?: number }): Promise<CursorHttp2StreamHandle> {
|
|
600
|
+
return this.client.openStream(request);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async dispose(): Promise<void> {
|
|
604
|
+
await this.#client?.dispose();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
class NativeHttp2CursorClient implements CursorHttp2Client {
|
|
609
|
+
constructor(readonly binding: CursorH2NativeBinding) {}
|
|
610
|
+
|
|
611
|
+
async requestUnary(request: { readonly baseUrl: string; readonly path: string; readonly headers: Record<string, string>; readonly body: Uint8Array; readonly signal?: AbortSignal; readonly timeoutMs?: number }): Promise<CursorHttp2UnaryResponse> {
|
|
612
|
+
if (request.signal?.aborted) throw new CursorTransportError("Aborted", "Cursor native HTTP/2 request aborted before start.");
|
|
613
|
+
const operationId = nextNativeOperationId();
|
|
614
|
+
try {
|
|
615
|
+
const response = await raceWithAbort(
|
|
616
|
+
this.binding.cursorH2RequestUnary(JSON.stringify({ baseUrl: request.baseUrl, path: request.path, headers: request.headers, operationId, timeoutMs: request.timeoutMs }), Buffer.from(request.body)),
|
|
617
|
+
request.signal,
|
|
618
|
+
"Cursor native HTTP/2 request aborted.",
|
|
619
|
+
undefined,
|
|
620
|
+
() => this.binding.cursorH2CancelOperation(operationId),
|
|
621
|
+
);
|
|
622
|
+
return {
|
|
623
|
+
statusCode: nativeStatusCode(response.statusCode ?? response.status_code),
|
|
624
|
+
headers: parseNativeHeaders(response.headersJson ?? response.headers_json),
|
|
625
|
+
body: new Uint8Array(response.body),
|
|
626
|
+
};
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw toTransportError(error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async openStream(request: { readonly baseUrl: string; readonly path: string; readonly headers: Record<string, string>; readonly signal?: AbortSignal; readonly initialBody?: Uint8Array; readonly timeoutMs?: number }): Promise<CursorHttp2StreamHandle> {
|
|
633
|
+
if (request.signal?.aborted) throw new CursorTransportError("Aborted", "Cursor native HTTP/2 stream aborted before start.");
|
|
634
|
+
const operationId = nextNativeOperationId();
|
|
635
|
+
try {
|
|
636
|
+
const stream = await raceWithAbort(
|
|
637
|
+
this.binding.cursorH2OpenStream(
|
|
638
|
+
JSON.stringify({ baseUrl: request.baseUrl, path: request.path, headers: request.headers, operationId, timeoutMs: request.timeoutMs }),
|
|
639
|
+
request.initialBody ? Buffer.from(request.initialBody) : null,
|
|
640
|
+
),
|
|
641
|
+
request.signal,
|
|
642
|
+
"Cursor native HTTP/2 stream aborted while opening.",
|
|
643
|
+
(lateStream) => lateStream.cancel().catch(() => undefined),
|
|
644
|
+
() => this.binding.cursorH2CancelOperation(operationId),
|
|
645
|
+
);
|
|
646
|
+
return new NativeCursorStreamHandle(stream);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
throw toTransportError(error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async dispose(): Promise<void> {
|
|
653
|
+
// Native streams own their HTTP/2 sessions and dispose when closed/cancelled.
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function createNativeCursorHttp2ClientForTest(binding: CursorH2NativeBinding): CursorHttp2Client {
|
|
658
|
+
return new NativeHttp2CursorClient(binding);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
class NativeCursorStreamHandle implements CursorHttp2StreamHandle {
|
|
662
|
+
readonly frames: AsyncIterable<Uint8Array>;
|
|
663
|
+
#closed = false;
|
|
664
|
+
|
|
665
|
+
constructor(readonly stream: CursorH2NativeStream) {
|
|
666
|
+
this.frames = this.createFrames();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async write(data: Uint8Array, options: CursorWriteOptions = {}): Promise<void> {
|
|
670
|
+
if (this.#closed) throw new CursorTransportError("ProtocolError", "Cannot write to a closed Cursor native stream.");
|
|
671
|
+
if (options.signal?.aborted) throw new CursorTransportError("Aborted", "Cursor native stream write aborted before start.");
|
|
672
|
+
try {
|
|
673
|
+
await raceWithAbort(
|
|
674
|
+
withTimeout(
|
|
675
|
+
this.stream.write(Buffer.from(data), options.timeoutMs ?? null),
|
|
676
|
+
options.timeoutMs,
|
|
677
|
+
"Cursor native stream write timed out.",
|
|
678
|
+
),
|
|
679
|
+
options.signal,
|
|
680
|
+
"Cursor native stream write aborted.",
|
|
681
|
+
undefined,
|
|
682
|
+
() => this.cancel().catch(() => undefined),
|
|
683
|
+
);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
await this.cancel().catch(() => undefined);
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async close(): Promise<void> {
|
|
691
|
+
if (this.#closed) return;
|
|
692
|
+
this.#closed = true;
|
|
693
|
+
await this.stream.finishInput();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async cancel(): Promise<void> {
|
|
697
|
+
if (this.#closed) return;
|
|
698
|
+
this.#closed = true;
|
|
699
|
+
await this.stream.cancel();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private async *createFrames(): AsyncIterable<Uint8Array> {
|
|
703
|
+
while (true) {
|
|
704
|
+
const frame = await this.stream.nextFrame();
|
|
705
|
+
if (!frame) break;
|
|
706
|
+
yield new Uint8Array(frame);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function nativeStatusCode(value: number | undefined): number | undefined {
|
|
712
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function parseNativeHeaders(headersJson: string | undefined): Record<string, string> {
|
|
716
|
+
if (!headersJson) return {};
|
|
717
|
+
const parsed = parseJsonObject(headersJson);
|
|
718
|
+
if (!parsed) return {};
|
|
719
|
+
const headers: Record<string, string> = {};
|
|
720
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
721
|
+
if (typeof value === "string") headers[key] = value;
|
|
722
|
+
}
|
|
723
|
+
return headers;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const textDecoder = new TextDecoder();
|
|
727
|
+
|
|
728
|
+
export function sanitizeCursorTransportError(error: Error, secrets: readonly string[] = []): Error {
|
|
729
|
+
const message = sanitizeDiagnosticText(error.message, secrets);
|
|
730
|
+
return error instanceof CursorTransportError ? new CursorTransportError(error.code, message) : new CursorTransportError("ProtocolError", message);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function throwIfCursorEndStreamError(data: Uint8Array, secrets: readonly string[]): void {
|
|
734
|
+
let parsed: JsonObject;
|
|
735
|
+
try {
|
|
736
|
+
const value = JSON.parse(textDecoder.decode(data)) as unknown;
|
|
737
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
738
|
+
parsed = value as JsonObject;
|
|
739
|
+
} catch {
|
|
740
|
+
throw new CursorTransportError("ProtocolError", "Failed to parse Cursor Connect end stream.");
|
|
741
|
+
}
|
|
742
|
+
const errorValue = parsed.error;
|
|
743
|
+
if (!errorValue) return;
|
|
744
|
+
if (typeof errorValue !== "object" || Array.isArray(errorValue)) {
|
|
745
|
+
throw new CursorTransportError("CursorApiRejected", `Cursor stream ended with unknown: ${sanitizeDiagnosticText(String(errorValue), secrets)}.`);
|
|
746
|
+
}
|
|
747
|
+
const error = errorValue as JsonObject;
|
|
748
|
+
const code = readStringField(error, "code") ?? "unknown";
|
|
749
|
+
const message = readStringField(error, "message") ?? "Unknown error";
|
|
750
|
+
throw new CursorTransportError(classifyConnectErrorCode(code), `Cursor stream ended with ${code}: ${sanitizeDiagnosticText(message, secrets)}.`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function classifyConnectErrorCode(code: string): CursorTransportErrorCode {
|
|
754
|
+
if (code === "unauthenticated") return "Unauthorized";
|
|
755
|
+
if (code === "canceled") return "Aborted";
|
|
756
|
+
if (code === "resource_exhausted" || code === "unavailable") return "NetworkError";
|
|
757
|
+
return "CursorApiRejected";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function assertSuccessfulStatus(statusCode: number | undefined, body: Uint8Array, secrets: readonly string[]): void {
|
|
761
|
+
if (statusCode === undefined || (statusCode >= 200 && statusCode < 300)) return;
|
|
762
|
+
const detail = sanitizeDiagnosticText(textDecoder.decode(body), secrets);
|
|
763
|
+
const versionHint = cursorClientVersionHint(statusCode);
|
|
764
|
+
const message = `Cursor API rejected request with HTTP ${statusCode}${detail ? `: ${detail}` : ""}${versionHint}`;
|
|
765
|
+
if (statusCode === 401 || statusCode === 403) throw new CursorTransportError("Unauthorized", message);
|
|
766
|
+
throw new CursorTransportError("CursorApiRejected", message);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function cursorClientVersionHint(statusCode: number): string {
|
|
770
|
+
if (statusCode !== 403 && statusCode !== 426) return "";
|
|
771
|
+
return ` Cursor may be rejecting the bundled Cursor CLI-compatible client version (${CURSOR_CLIENT_VERSION}); refresh CURSOR_CLIENT_VERSION from current Cursor CLI traffic if authentication still succeeds in Cursor itself.`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function toError(error: unknown): Error {
|
|
775
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function toTransportError(error: unknown): CursorTransportError {
|
|
779
|
+
if (error instanceof CursorTransportError) return error;
|
|
780
|
+
return new CursorTransportError("NetworkError", toError(error).message);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function concatBytes(...parts: readonly Uint8Array[]): Uint8Array {
|
|
784
|
+
const output = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
785
|
+
let offset = 0;
|
|
786
|
+
for (const part of parts) {
|
|
787
|
+
output.set(part, offset);
|
|
788
|
+
offset += part.length;
|
|
789
|
+
}
|
|
790
|
+
return output;
|
|
791
|
+
}
|