cursor-opencode-provider 0.7.1 → 0.7.3
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/README.md +8 -5
- package/dist/context/agents.js +1 -1
- package/dist/context/build.d.ts +2 -0
- package/dist/context/build.js +22 -10
- package/dist/context/epoch.d.ts +59 -0
- package/dist/context/epoch.js +224 -0
- package/dist/context/frozen.d.ts +4 -2
- package/dist/context/frozen.js +31 -11
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/overlay.d.ts +44 -0
- package/dist/context/overlay.js +96 -0
- package/dist/debug.d.ts +9 -1
- package/dist/debug.js +46 -5
- package/dist/host-event-bridge.d.ts +22 -0
- package/dist/host-event-bridge.js +22 -0
- package/dist/image-input.js +36 -11
- package/dist/index.d.ts +3 -0
- package/dist/language-model.d.ts +51 -3
- package/dist/language-model.js +569 -140
- package/dist/model-config.js +5 -1
- package/dist/plugin-opencode2.js +10 -4
- package/dist/plugin.js +26 -15
- package/dist/pricing-data.d.ts +40 -0
- package/dist/pricing-data.js +40 -0
- package/dist/protocol/ask-question.d.ts +8 -9
- package/dist/protocol/ask-question.js +8 -9
- package/dist/protocol/conversation-bind.js +18 -8
- package/dist/protocol/create-plan.d.ts +6 -7
- package/dist/protocol/create-plan.js +4 -5
- package/dist/protocol/messages.js +18 -1
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/switch-mode.d.ts +3 -4
- package/dist/protocol/switch-mode.js +21 -12
- package/dist/protocol/tool-call-bridge.d.ts +4 -1
- package/dist/protocol/tool-call-bridge.js +27 -5
- package/dist/protocol/tools.d.ts +4 -2
- package/dist/protocol/tools.js +346 -55
- package/dist/session-directory.d.ts +23 -4
- package/dist/session-directory.js +36 -4
- package/dist/session.d.ts +23 -3
- package/dist/session.js +68 -8
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/transport/connect.js +78 -10
- package/dist/transport/https-proxy.d.ts +56 -0
- package/dist/transport/https-proxy.js +348 -0
- package/dist/usage.d.ts +11 -0
- package/dist/usage.js +33 -7
- package/package.json +1 -1
|
@@ -5,13 +5,32 @@
|
|
|
5
5
|
* supplied once per invocation by the 1.x host. OpenCode 2.0 runs a single
|
|
6
6
|
* daemon across many projects, so `process.cwd()` captured at sdk-creation
|
|
7
7
|
* time is wrong for any session but the one open when the daemon started.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* The 2.0 runtime names a session directory in two places:
|
|
10
|
+
* - request header `x-opencode-directory` (per-request; preferred in the LM)
|
|
11
|
+
* - `ctx.session.get()` → flat `info.directory`, or legacy
|
|
12
|
+
* `info.location.directory`, reachable from `session.hook("context")`
|
|
13
|
+
*
|
|
14
|
+
* This module records the session-get value by id so the language model can
|
|
15
|
+
* fall back when the header is absent, same mechanism as `compaction-marker.ts`.
|
|
12
16
|
*
|
|
13
17
|
* Bounded so a long-lived server cannot accumulate ids for dead sessions.
|
|
14
18
|
*/
|
|
15
19
|
export declare function markSessionDirectory(sessionID: string, directory: string | undefined): void;
|
|
16
20
|
export declare function getSessionDirectory(sessionID: string | undefined): string | undefined;
|
|
17
21
|
export declare function clearSessionDirectories(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Active session workspace directory from OpenCode 2.0 request headers.
|
|
24
|
+
* Values may be URI-encoded.
|
|
25
|
+
*/
|
|
26
|
+
export declare function opencodeDirectoryHeader(headers: Record<string, string | undefined> | undefined): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the workspace root for a model turn.
|
|
29
|
+
* Prefer the per-request header, then the session mark, then static options/cwd.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveSessionWorkspaceRoot(input: {
|
|
32
|
+
sessionKey?: string;
|
|
33
|
+
headers?: Record<string, string | undefined>;
|
|
34
|
+
workspaceRoot?: string;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
}): string;
|
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* supplied once per invocation by the 1.x host. OpenCode 2.0 runs a single
|
|
6
6
|
* daemon across many projects, so `process.cwd()` captured at sdk-creation
|
|
7
7
|
* time is wrong for any session but the one open when the daemon started.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* The 2.0 runtime names a session directory in two places:
|
|
10
|
+
* - request header `x-opencode-directory` (per-request; preferred in the LM)
|
|
11
|
+
* - `ctx.session.get()` → flat `info.directory`, or legacy
|
|
12
|
+
* `info.location.directory`, reachable from `session.hook("context")`
|
|
13
|
+
*
|
|
14
|
+
* This module records the session-get value by id so the language model can
|
|
15
|
+
* fall back when the header is absent, same mechanism as `compaction-marker.ts`.
|
|
12
16
|
*
|
|
13
17
|
* Bounded so a long-lived server cannot accumulate ids for dead sessions.
|
|
14
18
|
*/
|
|
19
|
+
import path from "node:path";
|
|
15
20
|
const MAX_TRACKED_SESSIONS = 256;
|
|
16
21
|
const sessionDirectories = new Map();
|
|
17
22
|
export function markSessionDirectory(sessionID, directory) {
|
|
@@ -33,3 +38,30 @@ export function getSessionDirectory(sessionID) {
|
|
|
33
38
|
export function clearSessionDirectories() {
|
|
34
39
|
sessionDirectories.clear();
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Active session workspace directory from OpenCode 2.0 request headers.
|
|
43
|
+
* Values may be URI-encoded.
|
|
44
|
+
*/
|
|
45
|
+
export function opencodeDirectoryHeader(headers) {
|
|
46
|
+
if (!headers)
|
|
47
|
+
return undefined;
|
|
48
|
+
const raw = headers["x-opencode-directory"] ?? headers["X-Opencode-Directory"];
|
|
49
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const trimmed = raw.trim();
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(trimmed);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the workspace root for a model turn.
|
|
61
|
+
* Prefer the per-request header, then the session mark, then static options/cwd.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveSessionWorkspaceRoot(input) {
|
|
64
|
+
return path.resolve(opencodeDirectoryHeader(input.headers) ??
|
|
65
|
+
getSessionDirectory(input.sessionKey) ??
|
|
66
|
+
(input.workspaceRoot || input.cwd || process.cwd()));
|
|
67
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -131,6 +131,8 @@ export type CursorSession = {
|
|
|
131
131
|
stepCompletes: number;
|
|
132
132
|
displayToolCalls: number;
|
|
133
133
|
execRequests: number;
|
|
134
|
+
createPlanInTurn?: boolean;
|
|
135
|
+
switchModeInTurn?: boolean;
|
|
134
136
|
};
|
|
135
137
|
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
136
138
|
openCodeSessionId?: string;
|
|
@@ -186,6 +188,12 @@ export type CursorSession = {
|
|
|
186
188
|
* stream instead of emitting tool-call parts OpenCode will reject.
|
|
187
189
|
*/
|
|
188
190
|
allowTools: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* Host-enabled tool names for *this* Run (what actually arrived). May be a
|
|
193
|
+
* subset of the epoch advertisement when plan denies edit — advertise fully
|
|
194
|
+
* for cache stability, but refuse exec outside this set.
|
|
195
|
+
*/
|
|
196
|
+
permittedToolNames?: ReadonlySet<string>;
|
|
189
197
|
/**
|
|
190
198
|
* Best-effort held-Run activity counters used only for diagnostics. Displayed
|
|
191
199
|
* AI SDK usage comes from checkpoint occupancy (tool steps) and TurnEnded
|
|
@@ -245,12 +253,24 @@ export declare class SessionManager {
|
|
|
245
253
|
private readonly maxOpenSessions;
|
|
246
254
|
constructor(options?: SessionManagerOptions);
|
|
247
255
|
registerSession(session: CursorSession): void;
|
|
256
|
+
/** Live open Run for this OpenCode session id, if any. */
|
|
257
|
+
findOpenByOpenCodeSessionId(openCodeSessionId: string | undefined): CursorSession | undefined;
|
|
258
|
+
isActivelyPumping(session: CursorSession): boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Clear display-only bridged pendings that do not require a Cursor exec write.
|
|
261
|
+
* Used when the host starts a fresh user turn instead of returning the bridged
|
|
262
|
+
* tool result (e.g. human `continue` while a todowrite mirror is outstanding).
|
|
263
|
+
*
|
|
264
|
+
* @returns number of bridged pendings settled
|
|
265
|
+
*/
|
|
266
|
+
settleBridgedPending(session: CursorSession): number;
|
|
248
267
|
private isPumping;
|
|
249
268
|
/**
|
|
250
269
|
* Force-close the oldest idle open session(s) once registration exceeds the
|
|
251
|
-
* cap. Never closes a session with a live pull() mid-await
|
|
252
|
-
*
|
|
253
|
-
* active sessions just means the cap
|
|
270
|
+
* cap. Never closes a session with a live pull() mid-await, or one still
|
|
271
|
+
* holding pending execs (continuation / fresh-turn cancel+drain owns those).
|
|
272
|
+
* An unclosable backlog of genuinely active sessions just means the cap
|
|
273
|
+
* can't be enforced until one finishes.
|
|
254
274
|
*/
|
|
255
275
|
private enforceOpenSessionCap;
|
|
256
276
|
recordSemanticProgress(session: CursorSession, at?: number): void;
|
package/dist/session.js
CHANGED
|
@@ -109,10 +109,26 @@ export class SessionManager {
|
|
|
109
109
|
`— leaving it to finish or expire naturally instead of interrupting it`);
|
|
110
110
|
}
|
|
111
111
|
else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// Display-only bridged pendings do not need a Cursor exec write — settle
|
|
113
|
+
// them first so a todowrite-only prior can still be superseded cleanly.
|
|
114
|
+
const settled = this.settleBridgedPending(prior);
|
|
115
|
+
if (settled > 0) {
|
|
116
|
+
trace(`sessionManager.registerSession: settled ${settled} bridged pending on prior ` +
|
|
117
|
+
`session ${prior.sessionId} before supersede`);
|
|
118
|
+
}
|
|
119
|
+
if (prior.pending.size > 0) {
|
|
120
|
+
// Real execs still outstanding — preparePriorSessionForFreshTurn should
|
|
121
|
+
// have cancelled + drained. Do not mid-exec supersede.
|
|
122
|
+
trace(`sessionManager.registerSession: openCodeSessionId=${session.openCodeSessionId} ` +
|
|
123
|
+
`has prior session ${prior.sessionId} with ${prior.pending.size} still-pending ` +
|
|
124
|
+
`exec(s) — refusing supersede; leaving held Run for cancel/drain/continuation`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
trace(`sessionManager.registerSession: superseding stale session ${prior.sessionId} ` +
|
|
128
|
+
`(openCodeSessionId=${session.openCodeSessionId}, pending=${prior.pending.size}) ` +
|
|
129
|
+
`with new session ${session.sessionId}`);
|
|
130
|
+
this.close(prior, "superseded-by-new-run");
|
|
131
|
+
}
|
|
116
132
|
}
|
|
117
133
|
}
|
|
118
134
|
this.byOpenCodeSessionId.set(session.openCodeSessionId, session);
|
|
@@ -120,14 +136,54 @@ export class SessionManager {
|
|
|
120
136
|
this.enforceOpenSessionCap(session);
|
|
121
137
|
this.subscribeTerminal(session);
|
|
122
138
|
}
|
|
139
|
+
/** Live open Run for this OpenCode session id, if any. */
|
|
140
|
+
findOpenByOpenCodeSessionId(openCodeSessionId) {
|
|
141
|
+
if (!openCodeSessionId)
|
|
142
|
+
return undefined;
|
|
143
|
+
const session = this.byOpenCodeSessionId.get(openCodeSessionId);
|
|
144
|
+
return session && !session.closed ? session : undefined;
|
|
145
|
+
}
|
|
146
|
+
isActivelyPumping(session) {
|
|
147
|
+
return this.isPumping(session);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Clear display-only bridged pendings that do not require a Cursor exec write.
|
|
151
|
+
* Used when the host starts a fresh user turn instead of returning the bridged
|
|
152
|
+
* tool result (e.g. human `continue` while a todowrite mirror is outstanding).
|
|
153
|
+
*
|
|
154
|
+
* @returns number of bridged pendings settled
|
|
155
|
+
*/
|
|
156
|
+
settleBridgedPending(session) {
|
|
157
|
+
if (session.closed)
|
|
158
|
+
return 0;
|
|
159
|
+
let settled = 0;
|
|
160
|
+
for (const [execId, pending] of [...session.pending.entries()]) {
|
|
161
|
+
if (!pending.bridged)
|
|
162
|
+
continue;
|
|
163
|
+
if (pending.state === "claimed")
|
|
164
|
+
continue;
|
|
165
|
+
const key = this.key(session.sessionId, execId);
|
|
166
|
+
this.putTombstone(key, "delivered");
|
|
167
|
+
session.pending.delete(execId);
|
|
168
|
+
this.byExecId.delete(key);
|
|
169
|
+
settled++;
|
|
170
|
+
}
|
|
171
|
+
if (settled > 0) {
|
|
172
|
+
if (session.pending.size === 0)
|
|
173
|
+
this.recordSemanticProgress(session);
|
|
174
|
+
this.scheduleHardDeadline(session);
|
|
175
|
+
}
|
|
176
|
+
return settled;
|
|
177
|
+
}
|
|
123
178
|
isPumping(session) {
|
|
124
179
|
return session.pumpOwner != null || session.pumpActive;
|
|
125
180
|
}
|
|
126
181
|
/**
|
|
127
182
|
* Force-close the oldest idle open session(s) once registration exceeds the
|
|
128
|
-
* cap. Never closes a session with a live pull() mid-await
|
|
129
|
-
*
|
|
130
|
-
* active sessions just means the cap
|
|
183
|
+
* cap. Never closes a session with a live pull() mid-await, or one still
|
|
184
|
+
* holding pending execs (continuation / fresh-turn cancel+drain owns those).
|
|
185
|
+
* An unclosable backlog of genuinely active sessions just means the cap
|
|
186
|
+
* can't be enforced until one finishes.
|
|
131
187
|
*/
|
|
132
188
|
enforceOpenSessionCap(justRegistered) {
|
|
133
189
|
while (this.sessions.size > this.maxOpenSessions) {
|
|
@@ -135,13 +191,17 @@ export class SessionManager {
|
|
|
135
191
|
for (const candidate of this.sessions) {
|
|
136
192
|
if (candidate === justRegistered || this.isPumping(candidate))
|
|
137
193
|
continue;
|
|
194
|
+
// Held-open Runs with pending tools must not be cap-evicted — that is
|
|
195
|
+
// mid-exec supersede by another name and throws away the prefix.
|
|
196
|
+
if (candidate.pending.size > 0)
|
|
197
|
+
continue;
|
|
138
198
|
if (!oldest || candidate.createdAt < oldest.createdAt)
|
|
139
199
|
oldest = candidate;
|
|
140
200
|
}
|
|
141
201
|
if (!oldest) {
|
|
142
202
|
trace(`sessionManager.registerSession: open session cap exceeded ` +
|
|
143
203
|
`(${this.sessions.size} > ${this.maxOpenSessions}) but no idle session is ` +
|
|
144
|
-
`available to close — all remaining sessions are
|
|
204
|
+
`available to close — all remaining sessions are pumping or held-pending`);
|
|
145
205
|
return;
|
|
146
206
|
}
|
|
147
207
|
trace(`sessionManager.registerSession: open session cap exceeded ` +
|
package/dist/shared.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
|
4
4
|
export declare const CURSOR_PROVIDER_ID = "cursor";
|
|
5
5
|
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
6
|
export declare const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
7
|
+
/** OpenCode host rebuilt its history without a provider compaction turn. */
|
|
8
|
+
export declare const CURSOR_HISTORY_REWRITE_OPTION = "opencodeHistoryRewrite";
|
|
7
9
|
/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
|
|
8
10
|
export declare const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
|
|
9
11
|
export declare const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
package/dist/shared.js
CHANGED
|
@@ -4,6 +4,8 @@ export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
|
4
4
|
export const CURSOR_PROVIDER_ID = "cursor";
|
|
5
5
|
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
6
|
export const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
7
|
+
/** OpenCode host rebuilt its history without a provider compaction turn. */
|
|
8
|
+
export const CURSOR_HISTORY_REWRITE_OPTION = "opencodeHistoryRewrite";
|
|
7
9
|
/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
|
|
8
10
|
export const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
|
|
9
11
|
export const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
|
@@ -7,6 +7,7 @@ import { trace } from "../debug.js";
|
|
|
7
7
|
import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js";
|
|
8
8
|
import { withAbortDeadline } from "../deadline.js";
|
|
9
9
|
import http2 from "node:http2";
|
|
10
|
+
import { openProxiedTlsSocket, proxyEndpoint, resolveHttpsProxyUrl } from "./https-proxy.js";
|
|
10
11
|
const API_BASE = `https://${CURSOR_API_HOST}`;
|
|
11
12
|
const DEFAULT_UNARY_TIMEOUT_MS = 5_000;
|
|
12
13
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
@@ -407,40 +408,65 @@ function validateCachedSession(session, pingTimeoutMs) {
|
|
|
407
408
|
}
|
|
408
409
|
function connectSession(origin) {
|
|
409
410
|
return new Promise((resolve, reject) => {
|
|
410
|
-
const session = http2.connect(origin);
|
|
411
411
|
let settled = false;
|
|
412
|
-
|
|
412
|
+
let session;
|
|
413
|
+
let tunnelSocket;
|
|
414
|
+
const abort = new AbortController();
|
|
415
|
+
const clearConnectTimer = () => {
|
|
413
416
|
clearTimeout(timer);
|
|
417
|
+
};
|
|
418
|
+
const detachSessionListeners = () => {
|
|
419
|
+
if (!session)
|
|
420
|
+
return;
|
|
414
421
|
session.removeListener("error", onError);
|
|
415
422
|
session.removeListener("close", onClose);
|
|
416
423
|
session.removeListener("connect", onConnect);
|
|
417
424
|
};
|
|
425
|
+
const destroyTunnel = () => {
|
|
426
|
+
try {
|
|
427
|
+
tunnelSocket?.destroy();
|
|
428
|
+
}
|
|
429
|
+
catch { /* ignore */ }
|
|
430
|
+
tunnelSocket = undefined;
|
|
431
|
+
};
|
|
418
432
|
const fail = (error) => {
|
|
419
433
|
if (settled)
|
|
420
434
|
return;
|
|
421
435
|
settled = true;
|
|
422
|
-
|
|
423
|
-
dropSession(origin, session);
|
|
436
|
+
clearConnectTimer();
|
|
424
437
|
try {
|
|
425
|
-
|
|
438
|
+
abort.abort(error);
|
|
426
439
|
}
|
|
427
440
|
catch { /* ignore */ }
|
|
441
|
+
detachSessionListeners();
|
|
442
|
+
if (session) {
|
|
443
|
+
dropSession(origin, session);
|
|
444
|
+
try {
|
|
445
|
+
session.destroy();
|
|
446
|
+
}
|
|
447
|
+
catch { /* ignore */ }
|
|
448
|
+
}
|
|
449
|
+
destroyTunnel();
|
|
428
450
|
reject(error);
|
|
429
451
|
};
|
|
430
452
|
const onError = (error) => fail(toTransportError(error, "Cursor HTTP/2 connection failed"));
|
|
431
453
|
const onClose = () => fail(new CursorTransportError(`HTTP/2 connection to ${origin} closed before connecting`));
|
|
432
454
|
const onConnect = () => {
|
|
433
|
-
if (settled)
|
|
455
|
+
if (settled || !session)
|
|
434
456
|
return;
|
|
435
457
|
settled = true;
|
|
436
|
-
|
|
458
|
+
clearConnectTimer();
|
|
459
|
+
detachSessionListeners();
|
|
437
460
|
installSessionInvalidation(origin, session);
|
|
438
461
|
if (session.destroyed || session.closed) {
|
|
439
462
|
const error = new CursorTransportError(`HTTP/2 connection to ${origin} closed while connecting`);
|
|
440
463
|
invalidateSession(origin, session);
|
|
464
|
+
destroyTunnel();
|
|
441
465
|
reject(error);
|
|
442
466
|
return;
|
|
443
467
|
}
|
|
468
|
+
// The TLS socket (if any) is owned by the HTTP/2 session from here on.
|
|
469
|
+
tunnelSocket = undefined;
|
|
444
470
|
_http2SessionCreatedAt.set(session, Date.now());
|
|
445
471
|
_http2Sessions.set(origin, session);
|
|
446
472
|
trace(`h2 session connected: origin=${origin}`);
|
|
@@ -450,9 +476,51 @@ function connectSession(origin) {
|
|
|
450
476
|
fail(new CursorTransportError(`HTTP/2 connect to ${origin} timed out after ${CONNECT_TIMEOUT_MS}ms`));
|
|
451
477
|
}, CONNECT_TIMEOUT_MS);
|
|
452
478
|
timer.unref?.();
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
479
|
+
void (async () => {
|
|
480
|
+
try {
|
|
481
|
+
const { hostname, port: originPort } = new URL(origin);
|
|
482
|
+
const targetPort = originPort ? Number(originPort) : 443;
|
|
483
|
+
const proxy = resolveHttpsProxyUrl(hostname, process.env, targetPort);
|
|
484
|
+
if (proxy) {
|
|
485
|
+
const endpoint = proxyEndpoint(proxy);
|
|
486
|
+
trace(`h2 connect via HTTPS proxy: origin=${origin} proxy=${endpoint.host}:${endpoint.port}`);
|
|
487
|
+
const tlsSocket = await openProxiedTlsSocket({
|
|
488
|
+
proxy,
|
|
489
|
+
targetHost: hostname,
|
|
490
|
+
targetPort,
|
|
491
|
+
signal: abort.signal,
|
|
492
|
+
});
|
|
493
|
+
if (settled) {
|
|
494
|
+
try {
|
|
495
|
+
tlsSocket.destroy();
|
|
496
|
+
}
|
|
497
|
+
catch { /* ignore */ }
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
tunnelSocket = tlsSocket;
|
|
501
|
+
session = http2.connect(origin, {
|
|
502
|
+
createConnection: () => tlsSocket,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
session = http2.connect(origin);
|
|
507
|
+
}
|
|
508
|
+
if (settled) {
|
|
509
|
+
try {
|
|
510
|
+
session.destroy();
|
|
511
|
+
}
|
|
512
|
+
catch { /* ignore */ }
|
|
513
|
+
destroyTunnel();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
session.on("error", onError);
|
|
517
|
+
session.once("close", onClose);
|
|
518
|
+
session.once("connect", onConnect);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
fail(toTransportError(error, "Cursor HTTP/2 connection failed"));
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
456
524
|
});
|
|
457
525
|
}
|
|
458
526
|
export function getSession(baseURL, options = {}) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPS proxy helpers for the Cursor Run HTTP/2 transport.
|
|
3
|
+
*
|
|
4
|
+
* Unary RPCs use Bun `fetch()`, which already honors HTTPS_PROXY / NO_PROXY.
|
|
5
|
+
* `node:http2` does not, so Run sessions tunnel through an HTTP CONNECT proxy
|
|
6
|
+
* when those env vars apply. See GitHub issue #26.
|
|
7
|
+
*
|
|
8
|
+
* Important (Bun 1.3.x): do the CONNECT handshake on a native `net.Socket`,
|
|
9
|
+
* then `tls.connect({ socket })` on that same socket. Do not insert a custom
|
|
10
|
+
* Duplex between the proxy socket and TLS — destroying such a session mid-
|
|
11
|
+
* handshake can segfault Bun.
|
|
12
|
+
*/
|
|
13
|
+
import net from "node:net";
|
|
14
|
+
import tls from "node:tls";
|
|
15
|
+
export type ProxyEnv = Record<string, string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* HTTPS_PROXY / https_proxy for an HTTPS target. Like curl, HTTP_PROXY is not
|
|
18
|
+
* consulted for HTTPS. Only `http://` proxies can carry the CONNECT tunnel;
|
|
19
|
+
* `https://` (TLS to the proxy) and other schemes are ignored so the Run keeps
|
|
20
|
+
* its direct connection rather than failing on every connect.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveHttpsProxyUrl(targetHost: string, env?: ProxyEnv, targetPort?: number): URL | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* curl/Node-style NO_PROXY matching: `*`, exact host, optional `:port`, and
|
|
25
|
+
* leading-dot / bare-domain suffix forms (`.corp.example` / `corp.example`).
|
|
26
|
+
* A port-qualified entry only bypasses that port.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hostMatchesNoProxy(targetHost: string, noProxy: string | undefined, targetPort?: number): boolean;
|
|
29
|
+
/** Dialable proxy address: IPv6 literals without URL brackets, default port 80. */
|
|
30
|
+
export declare function proxyEndpoint(proxy: URL): {
|
|
31
|
+
host: string;
|
|
32
|
+
port: number;
|
|
33
|
+
};
|
|
34
|
+
export type OpenHttpsConnectTunnelOptions = {
|
|
35
|
+
proxy: URL;
|
|
36
|
+
targetHost: string;
|
|
37
|
+
targetPort?: number;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Injected for tests. */
|
|
40
|
+
connect?: typeof net.connect;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Open a plain TCP connection to the proxy and establish an HTTP CONNECT tunnel
|
|
44
|
+
* to `targetHost:targetPort`. Returns the tunneled socket ready for TLS.
|
|
45
|
+
*/
|
|
46
|
+
export declare function openHttpsConnectTunnel(options: OpenHttpsConnectTunnelOptions): Promise<net.Socket>;
|
|
47
|
+
export type OpenProxiedTlsSocketOptions = {
|
|
48
|
+
proxy: URL;
|
|
49
|
+
targetHost: string;
|
|
50
|
+
targetPort?: number;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
connect?: typeof net.connect;
|
|
53
|
+
tlsConnect?: typeof tls.connect;
|
|
54
|
+
};
|
|
55
|
+
/** CONNECT through the proxy, then complete TLS (ALPN h2) on the native socket. */
|
|
56
|
+
export declare function openProxiedTlsSocket(options: OpenProxiedTlsSocketOptions): Promise<tls.TLSSocket>;
|