cursor-opencode-provider 0.2.3 → 0.2.5

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/dist/errors.js ADDED
@@ -0,0 +1,199 @@
1
+ /** Structured provider failure with retry and transport diagnostics. */
2
+ export class CursorProviderError extends Error {
3
+ origin;
4
+ transient;
5
+ replaySafe;
6
+ statusCode;
7
+ grpcStatus;
8
+ rstCode;
9
+ code;
10
+ retryAfterMs;
11
+ constructor(message, options) {
12
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
13
+ this.name = "CursorProviderError";
14
+ this.origin = options.origin;
15
+ this.transient = options.transient;
16
+ this.replaySafe = options.replaySafe;
17
+ this.statusCode = options.statusCode;
18
+ this.grpcStatus = options.grpcStatus;
19
+ this.rstCode = options.rstCode;
20
+ this.code = options.code;
21
+ this.retryAfterMs = options.retryAfterMs;
22
+ }
23
+ }
24
+ export class CursorLocalCancellationError extends CursorProviderError {
25
+ constructor(message = "Cursor request cancelled locally", cause) {
26
+ super(message, {
27
+ origin: "local-cancel",
28
+ transient: false,
29
+ replaySafe: false,
30
+ cause,
31
+ });
32
+ this.name = "CursorLocalCancellationError";
33
+ }
34
+ }
35
+ export class CursorTransportError extends CursorProviderError {
36
+ constructor(message, options = {
37
+ transient: true,
38
+ replaySafe: true,
39
+ }) {
40
+ super(message, { ...options, origin: "transport" });
41
+ this.name = "CursorTransportError";
42
+ }
43
+ }
44
+ export class CursorServerError extends CursorProviderError {
45
+ constructor(message, options) {
46
+ super(message, { ...options, origin: "server" });
47
+ this.name = "CursorServerError";
48
+ }
49
+ }
50
+ export class CursorProtocolError extends CursorProviderError {
51
+ constructor(message, options = {}) {
52
+ super(message, {
53
+ ...options,
54
+ origin: "protocol",
55
+ transient: false,
56
+ replaySafe: false,
57
+ });
58
+ this.name = "CursorProtocolError";
59
+ }
60
+ }
61
+ export class CursorAuthError extends CursorProviderError {
62
+ constructor(message = "Cursor authentication failed; reauthenticate with Cursor", options = {}) {
63
+ super(message, {
64
+ ...options,
65
+ origin: "auth",
66
+ transient: false,
67
+ replaySafe: false,
68
+ });
69
+ this.name = "CursorAuthError";
70
+ }
71
+ }
72
+ export class CursorRetryExhaustedError extends CursorProviderError {
73
+ attempts;
74
+ constructor(attempts, last) {
75
+ super(`Cursor retry exhausted after ${attempts} attempts: ${last.message}`, {
76
+ origin: last.origin,
77
+ transient: false,
78
+ replaySafe: false,
79
+ statusCode: last.statusCode,
80
+ grpcStatus: last.grpcStatus,
81
+ rstCode: last.rstCode,
82
+ code: last.code,
83
+ retryAfterMs: last.retryAfterMs,
84
+ cause: last,
85
+ });
86
+ this.name = "CursorRetryExhaustedError";
87
+ this.attempts = attempts;
88
+ }
89
+ }
90
+ const TRANSIENT_NETWORK_CODES = new Set([
91
+ "ECONNABORTED",
92
+ "ECONNREFUSED",
93
+ "ECONNRESET",
94
+ "EHOSTUNREACH",
95
+ "ENETDOWN",
96
+ "ENETRESET",
97
+ "ENETUNREACH",
98
+ "ENOTFOUND",
99
+ "EPIPE",
100
+ "ERR_HTTP2_GOAWAY_SESSION",
101
+ "ERR_HTTP2_SESSION_ERROR",
102
+ "ERR_HTTP2_STREAM_CANCEL",
103
+ "ERR_HTTP2_STREAM_ERROR",
104
+ "ETIMEDOUT",
105
+ ]);
106
+ export function isTransientGrpcStatus(status) {
107
+ const normalized = String(status).toLowerCase().replaceAll("-", "_");
108
+ return (normalized === "8" ||
109
+ normalized === "13" ||
110
+ normalized === "14" ||
111
+ normalized === "internal" ||
112
+ normalized === "resource_exhausted" ||
113
+ normalized === "unavailable");
114
+ }
115
+ export function isAuthGrpcStatus(status) {
116
+ const normalized = String(status).toLowerCase().replaceAll("-", "_");
117
+ return (normalized === "7" ||
118
+ normalized === "16" ||
119
+ normalized === "permission_denied" ||
120
+ normalized === "unauthenticated");
121
+ }
122
+ export function cursorHttpError(operation, statusCode, diagnostics = {}) {
123
+ if (statusCode === 401 || statusCode === 403) {
124
+ return new CursorAuthError(`Cursor authentication failed (HTTP ${statusCode}); reauthenticate with Cursor`, { ...diagnostics, statusCode });
125
+ }
126
+ return new CursorServerError(`${operation} HTTP ${statusCode}`, {
127
+ ...diagnostics,
128
+ statusCode,
129
+ transient: statusCode === 429 || statusCode >= 500,
130
+ replaySafe: true,
131
+ });
132
+ }
133
+ export function cursorGrpcError(operation, grpcStatus, diagnostics = {}) {
134
+ if (isAuthGrpcStatus(grpcStatus)) {
135
+ return new CursorAuthError(`Cursor authentication failed (gRPC ${grpcStatus}); reauthenticate with Cursor`, { ...diagnostics, grpcStatus });
136
+ }
137
+ return new CursorServerError(`${operation} gRPC status ${grpcStatus}`, {
138
+ ...diagnostics,
139
+ grpcStatus,
140
+ transient: isTransientGrpcStatus(grpcStatus),
141
+ replaySafe: true,
142
+ });
143
+ }
144
+ export function errorCode(error) {
145
+ if (!error || typeof error !== "object")
146
+ return undefined;
147
+ const code = error.code;
148
+ return typeof code === "string" ? code : undefined;
149
+ }
150
+ export function toCursorProviderError(error, options = { replaySafe: false }) {
151
+ if (error instanceof CursorProviderError) {
152
+ error.replaySafe = options.replaySafe && error.replaySafe;
153
+ return error;
154
+ }
155
+ const code = errorCode(error);
156
+ const name = error instanceof Error ? error.name : "Error";
157
+ const message = error instanceof Error ? error.message : "";
158
+ if (name.startsWith("Auth") || /access token|api key|authentication/i.test(message)) {
159
+ return new CursorAuthError(undefined, { code, cause: error });
160
+ }
161
+ const httpStatus = /\bHTTP\s+(\d{3})\b/i.exec(message)?.[1];
162
+ if (httpStatus) {
163
+ const failure = cursorHttpError("Cursor request failed with", Number(httpStatus), { code });
164
+ failure.replaySafe = options.replaySafe && failure.replaySafe;
165
+ return failure;
166
+ }
167
+ if (code && TRANSIENT_NETWORK_CODES.has(code)) {
168
+ return new CursorTransportError(`Cursor transport failure (${code})`, {
169
+ transient: true,
170
+ replaySafe: options.replaySafe,
171
+ code,
172
+ cause: error,
173
+ });
174
+ }
175
+ if (error instanceof TypeError) {
176
+ return new CursorProtocolError(options.fallback ?? "Invalid Cursor provider configuration", {
177
+ code,
178
+ cause: error,
179
+ });
180
+ }
181
+ return new CursorProtocolError(options.fallback ?? `Cursor provider failure (${name})`, {
182
+ code,
183
+ cause: error,
184
+ });
185
+ }
186
+ export function retrySuppressedError(cause, reason, attempt, maxAttempts) {
187
+ const subject = cause.message.replace(/^Cursor Run stream /, "Cursor stream ");
188
+ return new CursorProviderError(`${subject} ${reason}; automatic retry unsafe (attempt ${attempt}/${maxAttempts})`, {
189
+ origin: cause.origin,
190
+ transient: false,
191
+ replaySafe: false,
192
+ statusCode: cause.statusCode,
193
+ grpcStatus: cause.grpcStatus,
194
+ rstCode: cause.rstCode,
195
+ code: cause.code,
196
+ retryAfterMs: cause.retryAfterMs,
197
+ cause,
198
+ });
199
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  import { CursorPlugin } from "./plugin.js";
2
+ import type { CursorContinuationOptions } from "./session.js";
3
+ export type CursorRetryOptions = {
4
+ /** Total attempts including the initial request. Default: 3. */
5
+ maxAttempts?: number;
6
+ /** Initial full-jitter backoff ceiling. Default: 500ms. */
7
+ baseDelayMs?: number;
8
+ /** Exponential backoff ceiling. Default: 8000ms. */
9
+ maxDelayMs?: number;
10
+ };
2
11
  export type CreateCursorOptions = {
3
12
  name: string;
4
13
  accessToken?: string;
@@ -14,9 +23,14 @@ export type CreateCursorOptions = {
14
23
  telemetryEnabled?: boolean;
15
24
  /** OpenCode project / worktree directory for request_context collectors. */
16
25
  workspaceRoot?: string;
26
+ /** Held-stream policy. Defaults: heartbeat 5s, semantic idle 120s, tool inactivity 10m. */
27
+ continuation?: CursorContinuationOptions;
28
+ /** Fresh-turn retry policy. Defaults: 3 attempts, 500ms base, 8000ms cap. */
29
+ retry?: CursorRetryOptions;
17
30
  };
18
31
  export declare function createCursor(options: CreateCursorOptions): {
19
32
  languageModel(modelId: string): import("@ai-sdk/provider").LanguageModelV3;
20
33
  };
21
34
  export { CursorPlugin };
35
+ export type { CursorContinuationOptions, CursorContinuationPolicy } from "./session.js";
22
36
  export default CursorPlugin;
package/dist/index.js CHANGED
@@ -11,6 +11,10 @@ export function createCursor(options) {
11
11
  }
12
12
  export { CursorPlugin };
13
13
  export default CursorPlugin;
14
+ // Keep root runtime exports plugin-safe. OpenCode's legacy plugin loader
15
+ // treats package-root exports as potential plugins, so extra public runtime
16
+ // APIs belong on subpaths such as "cursor-opencode-provider/errors".
17
+ //
14
18
  // CursorPluginV2 is NOT re-exported here — see plugin-v2.ts.
15
19
  // OpenCode's legacy plugin loader (getLegacyPlugins) iterates all exports
16
20
  // and calls getServerPlugin on each; the v2 define() return is not a
@@ -1,11 +1,29 @@
1
1
  import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider";
2
- import type { CreateCursorOptions } from "./index.js";
2
+ import type { CreateCursorOptions, CursorRetryOptions } from "./index.js";
3
3
  import { type OpencodeToolDef } from "./protocol/tools.js";
4
4
  import { type CursorSession } from "./session.js";
5
+ import { CursorProviderError } from "./errors.js";
5
6
  import type { SeedHistoryMessage } from "./protocol/request.js";
6
7
  export declare const MAX_TURN_STATE_SESSIONS = 256;
8
+ export type CursorRetryPolicy = {
9
+ maxAttempts: number;
10
+ baseDelayMs: number;
11
+ maxDelayMs: number;
12
+ };
13
+ export declare function resolveRetryPolicy(options: CursorRetryOptions | undefined): CursorRetryPolicy;
14
+ export declare function connectFrameError(payload: string): CursorProviderError;
7
15
  type V3Part = LanguageModelV3StreamPart;
8
16
  export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3;
17
+ export declare function pumpWithRecovery(input: {
18
+ initialSession: CursorSession;
19
+ controller: ReadableStreamDefaultController<V3Part>;
20
+ abortSignal?: AbortSignal;
21
+ promptTokens?: number;
22
+ retryPolicy?: CursorRetryPolicy;
23
+ recover: () => Promise<CursorSession>;
24
+ onSession?: (session: CursorSession) => void;
25
+ maxRecoveries?: number;
26
+ }): Promise<CursorSession>;
9
27
  /**
10
28
  * OpenCode re-sends the full tool-result history on every continuation. Prefer
11
29
  * the newest result that still has a live pending exec on its tagged session.
@@ -14,17 +32,27 @@ export declare function findContinuationSession(toolResults: Array<{
14
32
  sessionId: string;
15
33
  execId: number;
16
34
  }>): CursorSession | undefined;
35
+ /**
36
+ * Deliver trailing tool results onto a live continuation session.
37
+ * Returns the same session when writes succeed (or only bridged results were
38
+ * cleared). Returns undefined after closing the session when a write fails, so
39
+ * the caller can rebase onto a fresh Run instead of pumping a dead stream.
40
+ */
41
+ export declare function deliverContinuationResults(session: CursorSession, trailingToolResults: ExtractedToolResult[]): CursorSession | undefined;
17
42
  /**
18
43
  * Read the held-open stream, emitting stream parts, until the turn boundary:
19
44
  * - a tool call (exec_server_message) → emit tool-call, finish "tool-calls",
20
45
  * and KEEP the session open for the result on the next doStream call;
21
- * - turn_ended / stream end → finish "stop" and close the session.
46
+ * - turn_ended → finish "stop" and close the session;
47
+ * - transport EOF before turn_ended → throw for one fresh-Run recovery.
22
48
  */
23
49
  export declare function pump(session: CursorSession, controller: ReadableStreamDefaultController<V3Part>, ids: {
24
50
  textId: string;
25
51
  reasoningId: string;
52
+ promptTokens?: number;
26
53
  }, abortSignal?: AbortSignal): Promise<void>;
27
54
  type ExtractedToolResult = {
55
+ toolCallId: string;
28
56
  sessionId: string;
29
57
  execId: number;
30
58
  toolName: string;
@@ -42,15 +70,34 @@ export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOp
42
70
  * Redirect only to OpenCode tools that are genuinely advertised this turn;
43
71
  * compaction keeps its dedicated summary prompt unchanged.
44
72
  */
45
- export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean): string | undefined;
73
+ export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string): string | undefined;
46
74
  /** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
47
75
  export declare function estimateTokens(chars: number): number;
76
+ /** Estimate current-request prompt tokens from the complete serialized V3 prompt. */
77
+ export declare function estimatePromptTokens(prompt: LanguageModelV3CallOptions["prompt"]): number;
78
+ /** Preserve cumulative Cursor counters as diagnostics, never as AI SDK request usage. */
79
+ export declare function cursorTurnEndedProviderMetadata(te: Record<string, unknown>): {
80
+ cursor: {
81
+ usageVersion: number;
82
+ inputTokensRaw: number;
83
+ outputTokensRaw: number;
84
+ cacheReadRaw: number;
85
+ cacheWriteRaw: number;
86
+ reasoningTokensRaw: number;
87
+ };
88
+ };
48
89
  /**
49
- * Prior prompt turns for a seed ConversationStateStructure after compaction
50
- * reset. Drops the trailing user message (live action), but preserves tool
51
- * result payloads as labeled assistant observations for Cursor's text history.
90
+ * Prior prompt turns for a seed ConversationStateStructure. Tool results must
91
+ * never be replayed as assistant-authored prose: that teaches the model to
92
+ * counterfeit `Tool result (...)` text instead of emitting a real tool call.
93
+ * Normal rebases omit old results; compaction can retain all results and
94
+ * interrupted continuations retain only the trailing live result suffix as
95
+ * explicit OpenCode-host observations.
52
96
  */
53
- export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions["prompt"]): SeedHistoryMessage[];
97
+ export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions["prompt"], options?: {
98
+ preserveTrailingUser?: boolean;
99
+ toolResults?: "omit" | "all" | "trailing";
100
+ }): SeedHistoryMessage[];
54
101
  /** OpenCode session id header, if present. */
55
102
  export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined;
56
103
  /**