@playwo/opencode-cursor-oauth 0.0.0-dev.c80ebcb27754 → 0.0.0-dev.da5538092563

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 (76) hide show
  1. package/README.md +32 -83
  2. package/dist/auth.js +27 -3
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.js +2 -0
  5. package/dist/cursor/bidi-session.d.ts +12 -0
  6. package/dist/cursor/bidi-session.js +164 -0
  7. package/dist/cursor/config.d.ts +4 -0
  8. package/dist/cursor/config.js +4 -0
  9. package/dist/cursor/connect-framing.d.ts +10 -0
  10. package/dist/cursor/connect-framing.js +80 -0
  11. package/dist/cursor/headers.d.ts +6 -0
  12. package/dist/cursor/headers.js +16 -0
  13. package/dist/cursor/index.d.ts +5 -0
  14. package/dist/cursor/index.js +5 -0
  15. package/dist/cursor/unary-rpc.d.ts +12 -0
  16. package/dist/cursor/unary-rpc.js +124 -0
  17. package/dist/index.d.ts +2 -14
  18. package/dist/index.js +2 -229
  19. package/dist/logger.d.ts +7 -0
  20. package/dist/logger.js +150 -0
  21. package/dist/models.d.ts +3 -0
  22. package/dist/models.js +80 -54
  23. package/dist/openai/index.d.ts +3 -0
  24. package/dist/openai/index.js +3 -0
  25. package/dist/openai/messages.d.ts +39 -0
  26. package/dist/openai/messages.js +228 -0
  27. package/dist/openai/tools.d.ts +7 -0
  28. package/dist/openai/tools.js +58 -0
  29. package/dist/openai/types.d.ts +41 -0
  30. package/dist/openai/types.js +1 -0
  31. package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
  32. package/dist/plugin/cursor-auth-plugin.js +139 -0
  33. package/dist/proto/agent_pb.js +637 -319
  34. package/dist/provider/index.d.ts +2 -0
  35. package/dist/provider/index.js +2 -0
  36. package/dist/provider/model-cost.d.ts +9 -0
  37. package/dist/provider/model-cost.js +206 -0
  38. package/dist/provider/models.d.ts +8 -0
  39. package/dist/provider/models.js +86 -0
  40. package/dist/proxy/bridge-close-controller.d.ts +6 -0
  41. package/dist/proxy/bridge-close-controller.js +37 -0
  42. package/dist/proxy/bridge-non-streaming.d.ts +3 -0
  43. package/dist/proxy/bridge-non-streaming.js +123 -0
  44. package/dist/proxy/bridge-session.d.ts +5 -0
  45. package/dist/proxy/bridge-session.js +11 -0
  46. package/dist/proxy/bridge-streaming.d.ts +5 -0
  47. package/dist/proxy/bridge-streaming.js +409 -0
  48. package/dist/proxy/bridge.d.ts +3 -0
  49. package/dist/proxy/bridge.js +3 -0
  50. package/dist/proxy/chat-completion.d.ts +2 -0
  51. package/dist/proxy/chat-completion.js +153 -0
  52. package/dist/proxy/conversation-meta.d.ts +12 -0
  53. package/dist/proxy/conversation-meta.js +1 -0
  54. package/dist/proxy/conversation-state.d.ts +35 -0
  55. package/dist/proxy/conversation-state.js +95 -0
  56. package/dist/proxy/cursor-request.d.ts +6 -0
  57. package/dist/proxy/cursor-request.js +101 -0
  58. package/dist/proxy/index.d.ts +12 -0
  59. package/dist/proxy/index.js +12 -0
  60. package/dist/proxy/server.d.ts +6 -0
  61. package/dist/proxy/server.js +107 -0
  62. package/dist/proxy/sse.d.ts +5 -0
  63. package/dist/proxy/sse.js +5 -0
  64. package/dist/proxy/state-sync.d.ts +2 -0
  65. package/dist/proxy/state-sync.js +17 -0
  66. package/dist/proxy/stream-dispatch.d.ts +42 -0
  67. package/dist/proxy/stream-dispatch.js +641 -0
  68. package/dist/proxy/stream-state.d.ts +7 -0
  69. package/dist/proxy/stream-state.js +1 -0
  70. package/dist/proxy/title.d.ts +1 -0
  71. package/dist/proxy/title.js +103 -0
  72. package/dist/proxy/types.d.ts +32 -0
  73. package/dist/proxy/types.js +1 -0
  74. package/dist/proxy.d.ts +2 -19
  75. package/dist/proxy.js +2 -1221
  76. package/package.json +1 -2
package/dist/index.d.ts CHANGED
@@ -1,14 +1,2 @@
1
- /**
2
- * OpenCode Cursor Auth Plugin
3
- *
4
- * Enables using Cursor models (Claude, GPT, etc.) inside OpenCode via:
5
- * 1. Browser-based OAuth login to Cursor
6
- * 2. Local proxy translating OpenAI format → Cursor gRPC protocol
7
- */
8
- import type { Plugin } from "@opencode-ai/plugin";
9
- /**
10
- * OpenCode plugin that provides Cursor authentication and model access.
11
- * Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
12
- */
13
- export declare const CursorAuthPlugin: Plugin;
14
- export default CursorAuthPlugin;
1
+ export { CursorAuthPlugin } from "./plugin/cursor-auth-plugin";
2
+ export { CursorAuthPlugin as default } from "./plugin/cursor-auth-plugin";
package/dist/index.js CHANGED
@@ -1,229 +1,2 @@
1
- import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
2
- import { getCursorModels } from "./models";
3
- import { startProxy } from "./proxy";
4
- const CURSOR_PROVIDER_ID = "cursor";
5
- /**
6
- * OpenCode plugin that provides Cursor authentication and model access.
7
- * Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
8
- */
9
- export const CursorAuthPlugin = async (input) => {
10
- return {
11
- auth: {
12
- provider: CURSOR_PROVIDER_ID,
13
- async loader(getAuth, provider) {
14
- const auth = await getAuth();
15
- if (!auth || auth.type !== "oauth")
16
- return {};
17
- // Ensure we have a valid access token, refreshing if expired
18
- let accessToken = auth.access;
19
- if (!accessToken || auth.expires < Date.now()) {
20
- const refreshed = await refreshCursorToken(auth.refresh);
21
- await input.client.auth.set({
22
- path: { id: CURSOR_PROVIDER_ID },
23
- body: {
24
- type: "oauth",
25
- refresh: refreshed.refresh,
26
- access: refreshed.access,
27
- expires: refreshed.expires,
28
- },
29
- });
30
- accessToken = refreshed.access;
31
- }
32
- const models = await getCursorModels(accessToken);
33
- const port = await startProxy(async () => {
34
- const currentAuth = await getAuth();
35
- if (currentAuth.type !== "oauth") {
36
- throw new Error("Cursor auth not configured");
37
- }
38
- if (!currentAuth.access || currentAuth.expires < Date.now()) {
39
- const refreshed = await refreshCursorToken(currentAuth.refresh);
40
- await input.client.auth.set({
41
- path: { id: CURSOR_PROVIDER_ID },
42
- body: {
43
- type: "oauth",
44
- refresh: refreshed.refresh,
45
- access: refreshed.access,
46
- expires: refreshed.expires,
47
- },
48
- });
49
- return refreshed.access;
50
- }
51
- return currentAuth.access;
52
- }, models);
53
- if (provider) {
54
- provider.models = buildCursorProviderModels(models, port);
55
- }
56
- return {
57
- baseURL: `http://localhost:${port}/v1`,
58
- apiKey: "cursor-proxy",
59
- async fetch(requestInput, init) {
60
- if (init?.headers) {
61
- if (init.headers instanceof Headers) {
62
- init.headers.delete("authorization");
63
- }
64
- else if (Array.isArray(init.headers)) {
65
- init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
66
- }
67
- else {
68
- delete init.headers["authorization"];
69
- delete init.headers["Authorization"];
70
- }
71
- }
72
- return fetch(requestInput, init);
73
- },
74
- };
75
- },
76
- methods: [
77
- {
78
- type: "oauth",
79
- label: "Login with Cursor",
80
- async authorize() {
81
- const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
82
- return {
83
- url: loginUrl,
84
- instructions: "Complete login in your browser. This window will close automatically.",
85
- method: "auto",
86
- async callback() {
87
- const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
88
- return {
89
- type: "success",
90
- refresh: refreshToken,
91
- access: accessToken,
92
- expires: getTokenExpiry(accessToken),
93
- };
94
- },
95
- };
96
- },
97
- },
98
- ],
99
- },
100
- };
101
- };
102
- function buildCursorProviderModels(models, port) {
103
- return Object.fromEntries(models.map((model) => [
104
- model.id,
105
- {
106
- id: model.id,
107
- providerID: CURSOR_PROVIDER_ID,
108
- api: {
109
- id: model.id,
110
- url: `http://localhost:${port}/v1`,
111
- npm: "@ai-sdk/openai-compatible",
112
- },
113
- name: model.name,
114
- capabilities: {
115
- temperature: true,
116
- reasoning: model.reasoning,
117
- attachment: false,
118
- toolcall: true,
119
- input: {
120
- text: true,
121
- audio: false,
122
- image: false,
123
- video: false,
124
- pdf: false,
125
- },
126
- output: {
127
- text: true,
128
- audio: false,
129
- image: false,
130
- video: false,
131
- pdf: false,
132
- },
133
- interleaved: false,
134
- },
135
- cost: estimateModelCost(model.id),
136
- limit: {
137
- context: model.contextWindow,
138
- output: model.maxTokens,
139
- },
140
- status: "active",
141
- options: {},
142
- headers: {},
143
- release_date: "",
144
- variants: {},
145
- },
146
- ]));
147
- }
148
- // $/M token rates from cursor.com/docs/models-and-pricing
149
- const MODEL_COST_TABLE = {
150
- // Anthropic
151
- "claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
152
- "claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },
153
- "claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } },
154
- "claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
155
- "claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
156
- "claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
157
- "claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } },
158
- "claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
159
- // Cursor
160
- "composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
161
- "composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } },
162
- "composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } },
163
- "composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } },
164
- // Google
165
- "gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } },
166
- "gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } },
167
- "gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
168
- "gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
169
- "gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
170
- // OpenAI
171
- "gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
172
- "gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } },
173
- "gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
174
- "gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
175
- "gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
176
- "gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
177
- "gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
178
- "gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
179
- "gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
180
- "gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
181
- "gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } },
182
- "gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } },
183
- "gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } },
184
- // xAI
185
- "grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } },
186
- // Moonshot
187
- "kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } },
188
- };
189
- // Most-specific first
190
- const MODEL_COST_PATTERNS = [
191
- { match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] },
192
- { match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] },
193
- { match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] },
194
- { match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
195
- { match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
196
- { match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] },
197
- { match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] },
198
- { match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] },
199
- { match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] },
200
- { match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] },
201
- { match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] },
202
- { match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] },
203
- { match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] },
204
- { match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] },
205
- { match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] },
206
- { match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] },
207
- { match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] },
208
- { match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] },
209
- { match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
210
- { match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] },
211
- { match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] },
212
- { match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] },
213
- { match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
214
- { match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] },
215
- { match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] },
216
- ];
217
- const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } };
218
- function estimateModelCost(modelId) {
219
- const normalized = modelId.toLowerCase();
220
- const exact = MODEL_COST_TABLE[normalized];
221
- if (exact)
222
- return exact;
223
- const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, "");
224
- const strippedMatch = MODEL_COST_TABLE[stripped];
225
- if (strippedMatch)
226
- return strippedMatch;
227
- return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
228
- }
229
- export default CursorAuthPlugin;
1
+ export { CursorAuthPlugin } from "./plugin/cursor-auth-plugin";
2
+ export { CursorAuthPlugin as default } from "./plugin/cursor-auth-plugin";
@@ -0,0 +1,7 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function configurePluginLogger(input: PluginInput): void;
3
+ export declare function errorDetails(error: unknown): Record<string, unknown>;
4
+ export declare function logPluginWarn(message: string, extra?: Record<string, unknown>): void;
5
+ export declare function logPluginInfo(message: string, extra?: Record<string, unknown>): void;
6
+ export declare function logPluginError(message: string, extra?: Record<string, unknown>): void;
7
+ export declare function flushPluginLogs(): Promise<void>;
package/dist/logger.js ADDED
@@ -0,0 +1,150 @@
1
+ const PLUGIN_LOG_SERVICE = "opencode-cursor-oauth";
2
+ const MAX_STRING_LENGTH = 1_500;
3
+ const MAX_ARRAY_LENGTH = 20;
4
+ const MAX_OBJECT_KEYS = 25;
5
+ let currentLogger;
6
+ let pendingLogWrites = Promise.resolve();
7
+ export function configurePluginLogger(input) {
8
+ currentLogger = {
9
+ client: input.client,
10
+ directory: input.directory,
11
+ };
12
+ }
13
+ export function errorDetails(error) {
14
+ if (error instanceof Error) {
15
+ return {
16
+ errorName: error.name,
17
+ errorMessage: error.message,
18
+ errorStack: error.stack,
19
+ errorCause: serializeValue(error.cause, 1),
20
+ };
21
+ }
22
+ return {
23
+ errorType: typeof error,
24
+ errorValue: serializeValue(error, 1),
25
+ };
26
+ }
27
+ export function logPluginWarn(message, extra = {}) {
28
+ logPlugin("warn", message, extra);
29
+ }
30
+ export function logPluginInfo(message, extra = {}) {
31
+ logPlugin("info", message, extra);
32
+ }
33
+ export function logPluginError(message, extra = {}) {
34
+ logPlugin("error", message, extra);
35
+ }
36
+ export function flushPluginLogs() {
37
+ return pendingLogWrites;
38
+ }
39
+ function logPlugin(level, message, extra) {
40
+ const serializedExtra = serializeValue(extra, 0);
41
+ writeConsoleLog(level, message, serializedExtra);
42
+ if (!currentLogger?.client?.app?.log) {
43
+ return;
44
+ }
45
+ pendingLogWrites = pendingLogWrites
46
+ .catch(() => { })
47
+ .then(async () => {
48
+ try {
49
+ await currentLogger?.client.app.log({
50
+ query: { directory: currentLogger.directory },
51
+ body: {
52
+ service: PLUGIN_LOG_SERVICE,
53
+ level,
54
+ message,
55
+ extra: serializedExtra,
56
+ },
57
+ });
58
+ }
59
+ catch (logError) {
60
+ writeConsoleLog("warn", "Failed to forward plugin log to OpenCode", {
61
+ originalLevel: level,
62
+ originalMessage: message,
63
+ ...errorDetails(logError),
64
+ });
65
+ }
66
+ });
67
+ }
68
+ function writeConsoleLog(level, message, extra) {
69
+ const prefix = `[${PLUGIN_LOG_SERVICE}] ${message}`;
70
+ const suffix = Object.keys(extra).length > 0 ? ` ${JSON.stringify(extra)}` : "";
71
+ if (level === "error") {
72
+ console.error(`${prefix}${suffix}`);
73
+ return;
74
+ }
75
+ console.warn(`${prefix}${suffix}`);
76
+ }
77
+ function serializeValue(value, depth, seen = new WeakSet()) {
78
+ if (value === null || value === undefined)
79
+ return value;
80
+ if (typeof value === "string")
81
+ return truncateString(value);
82
+ const valueType = typeof value;
83
+ if (valueType === "number" || valueType === "boolean")
84
+ return value;
85
+ if (valueType === "bigint")
86
+ return value.toString();
87
+ if (valueType === "symbol")
88
+ return String(value);
89
+ if (valueType === "function")
90
+ return `[function ${value.name || "anonymous"}]`;
91
+ if (value instanceof URL)
92
+ return value.toString();
93
+ if (value instanceof Headers)
94
+ return Object.fromEntries(value.entries());
95
+ if (value instanceof Error) {
96
+ return {
97
+ name: value.name,
98
+ message: value.message,
99
+ stack: truncateString(value.stack),
100
+ cause: serializeValue(value.cause, depth + 1, seen),
101
+ };
102
+ }
103
+ if (value instanceof Uint8Array) {
104
+ return serializeBinary(value);
105
+ }
106
+ if (Array.isArray(value)) {
107
+ if (depth >= 3)
108
+ return `[array(${value.length})]`;
109
+ return value
110
+ .slice(0, MAX_ARRAY_LENGTH)
111
+ .map((entry) => serializeValue(entry, depth + 1, seen));
112
+ }
113
+ if (typeof value === "object") {
114
+ if (seen.has(value))
115
+ return "[circular]";
116
+ seen.add(value);
117
+ if (depth >= 3) {
118
+ return `[object ${value.constructor?.name || "Object"}]`;
119
+ }
120
+ const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
121
+ return Object.fromEntries(entries.map(([key, entry]) => [
122
+ key,
123
+ serializeValue(entry, depth + 1, seen),
124
+ ]));
125
+ }
126
+ return String(value);
127
+ }
128
+ function serializeBinary(value) {
129
+ const text = new TextDecoder().decode(value);
130
+ const printable = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(text);
131
+ if (printable) {
132
+ return {
133
+ type: "uint8array",
134
+ length: value.length,
135
+ text: truncateString(text),
136
+ };
137
+ }
138
+ return {
139
+ type: "uint8array",
140
+ length: value.length,
141
+ base64: truncateString(Buffer.from(value).toString("base64")),
142
+ };
143
+ }
144
+ function truncateString(value) {
145
+ if (value === undefined)
146
+ return undefined;
147
+ if (value.length <= MAX_STRING_LENGTH)
148
+ return value;
149
+ return `${value.slice(0, MAX_STRING_LENGTH - 3)}...`;
150
+ }
package/dist/models.d.ts CHANGED
@@ -5,6 +5,9 @@ export interface CursorModel {
5
5
  contextWindow: number;
6
6
  maxTokens: number;
7
7
  }
8
+ export declare class CursorModelDiscoveryError extends Error {
9
+ constructor(message: string);
10
+ }
8
11
  export declare function getCursorModels(apiKey: string): Promise<CursorModel[]>;
9
12
  /** @internal Test-only. */
10
13
  export declare function clearModelCache(): void;
package/dist/models.js CHANGED
@@ -1,13 +1,10 @@
1
- /**
2
- * Cursor model discovery via GetUsableModels.
3
- * Uses the H2 bridge for transport. Falls back to a hardcoded list
4
- * when discovery fails.
5
- */
6
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
7
2
  import { z } from "zod";
8
- import { callCursorUnaryRpc } from "./proxy";
3
+ import { callCursorUnaryRpc, decodeConnectUnaryBody } from "./cursor";
4
+ import { errorDetails, logPluginError, logPluginWarn } from "./logger";
9
5
  import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
10
6
  const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
7
+ const MODEL_DISCOVERY_TIMEOUT_MS = 5_000;
11
8
  const DEFAULT_CONTEXT_WINDOW = 200_000;
12
9
  const DEFAULT_MAX_TOKENS = 64_000;
13
10
  const CursorModelDetailsSchema = z.object({
@@ -22,24 +19,12 @@ const CursorModelDetailsSchema = z.object({
22
19
  .transform((aliases) => (aliases ?? []).filter((alias) => typeof alias === "string")),
23
20
  thinkingDetails: z.unknown().optional(),
24
21
  });
25
- const FALLBACK_MODELS = [
26
- // Composer models
27
- { id: "composer-1", name: "Composer 1", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
28
- { id: "composer-1.5", name: "Composer 1.5", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
29
- // Claude models
30
- { id: "claude-4.6-opus-high", name: "Claude 4.6 Opus", reasoning: true, contextWindow: 200_000, maxTokens: 128_000 },
31
- { id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
32
- { id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
33
- // GPT models
34
- { id: "gpt-5.4-medium", name: "GPT-5.4", reasoning: true, contextWindow: 272_000, maxTokens: 128_000 },
35
- { id: "gpt-5.2", name: "GPT-5.2", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
36
- { id: "gpt-5.2-codex", name: "GPT-5.2 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
37
- { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
38
- { id: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark", reasoning: true, contextWindow: 128_000, maxTokens: 128_000 },
39
- // Other models
40
- { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 },
41
- { id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 },
42
- ];
22
+ export class CursorModelDiscoveryError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "CursorModelDiscoveryError";
26
+ }
27
+ }
43
28
  async function fetchCursorUsableModels(apiKey) {
44
29
  try {
45
30
  const requestPayload = create(GetUsableModelsRequestSchema, {});
@@ -48,18 +33,51 @@ async function fetchCursorUsableModels(apiKey) {
48
33
  accessToken: apiKey,
49
34
  rpcPath: GET_USABLE_MODELS_PATH,
50
35
  requestBody,
36
+ timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
51
37
  });
52
- if (response.timedOut || response.exitCode !== 0 || response.body.length === 0) {
53
- return null;
38
+ if (response.timedOut) {
39
+ logPluginError("Cursor model discovery timed out", {
40
+ rpcPath: GET_USABLE_MODELS_PATH,
41
+ timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
42
+ });
43
+ throw new CursorModelDiscoveryError(`Cursor model discovery timed out after ${MODEL_DISCOVERY_TIMEOUT_MS}ms.`);
44
+ }
45
+ if (response.exitCode !== 0) {
46
+ logPluginError("Cursor model discovery HTTP failure", {
47
+ rpcPath: GET_USABLE_MODELS_PATH,
48
+ exitCode: response.exitCode,
49
+ responseBody: response.body,
50
+ });
51
+ throw new CursorModelDiscoveryError(buildDiscoveryHttpError(response.exitCode, response.body));
52
+ }
53
+ if (response.body.length === 0) {
54
+ logPluginWarn("Cursor model discovery returned an empty response", {
55
+ rpcPath: GET_USABLE_MODELS_PATH,
56
+ });
57
+ throw new CursorModelDiscoveryError("Cursor model discovery returned an empty response.");
54
58
  }
55
59
  const decoded = decodeGetUsableModelsResponse(response.body);
56
- if (!decoded)
57
- return null;
60
+ if (!decoded) {
61
+ logPluginError("Cursor model discovery returned an unreadable response", {
62
+ rpcPath: GET_USABLE_MODELS_PATH,
63
+ responseBody: response.body,
64
+ });
65
+ throw new CursorModelDiscoveryError("Cursor model discovery returned an unreadable response.");
66
+ }
58
67
  const models = normalizeCursorModels(decoded.models);
59
- return models.length > 0 ? models : null;
68
+ if (models.length === 0) {
69
+ throw new CursorModelDiscoveryError("Cursor model discovery returned no usable models.");
70
+ }
71
+ return models;
60
72
  }
61
- catch {
62
- return null;
73
+ catch (error) {
74
+ if (error instanceof CursorModelDiscoveryError)
75
+ throw error;
76
+ logPluginError("Cursor model discovery crashed", {
77
+ rpcPath: GET_USABLE_MODELS_PATH,
78
+ ...errorDetails(error),
79
+ });
80
+ throw new CursorModelDiscoveryError("Cursor model discovery failed.");
63
81
  }
64
82
  }
65
83
  let cachedModels = null;
@@ -67,13 +85,43 @@ export async function getCursorModels(apiKey) {
67
85
  if (cachedModels)
68
86
  return cachedModels;
69
87
  const discovered = await fetchCursorUsableModels(apiKey);
70
- cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;
88
+ cachedModels = discovered;
71
89
  return cachedModels;
72
90
  }
73
91
  /** @internal Test-only. */
74
92
  export function clearModelCache() {
75
93
  cachedModels = null;
76
94
  }
95
+ function buildDiscoveryHttpError(exitCode, body) {
96
+ const detail = extractDiscoveryErrorDetail(body);
97
+ const protocolHint = exitCode === 464
98
+ ? " Likely protocol mismatch: Cursor appears to expect an HTTP/2 Connect unary request."
99
+ : "";
100
+ if (!detail) {
101
+ return `Cursor model discovery failed with HTTP ${exitCode}.${protocolHint}`;
102
+ }
103
+ return `Cursor model discovery failed with HTTP ${exitCode}: ${detail}.${protocolHint}`;
104
+ }
105
+ function extractDiscoveryErrorDetail(body) {
106
+ if (body.length === 0)
107
+ return null;
108
+ const text = new TextDecoder().decode(body).trim();
109
+ if (!text)
110
+ return null;
111
+ try {
112
+ const parsed = JSON.parse(text);
113
+ const code = typeof parsed.code === "string" ? parsed.code : undefined;
114
+ const message = typeof parsed.message === "string" ? parsed.message : undefined;
115
+ if (message && code)
116
+ return `${message} (${code})`;
117
+ if (message)
118
+ return message;
119
+ if (code)
120
+ return code;
121
+ }
122
+ catch { }
123
+ return text.length > 200 ? `${text.slice(0, 197)}...` : text;
124
+ }
77
125
  function decodeGetUsableModelsResponse(payload) {
78
126
  try {
79
127
  return fromBinary(GetUsableModelsResponseSchema, payload);
@@ -90,28 +138,6 @@ function decodeGetUsableModelsResponse(payload) {
90
138
  }
91
139
  }
92
140
  }
93
- function decodeConnectUnaryBody(payload) {
94
- if (payload.length < 5)
95
- return null;
96
- let offset = 0;
97
- while (offset + 5 <= payload.length) {
98
- const flags = payload[offset];
99
- const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
100
- const messageLength = view.getUint32(1, false);
101
- const frameEnd = offset + 5 + messageLength;
102
- if (frameEnd > payload.length)
103
- return null;
104
- // Compression flag
105
- if ((flags & 0b0000_0001) !== 0)
106
- return null;
107
- // End-of-stream flag — skip trailer frames
108
- if ((flags & 0b0000_0010) === 0) {
109
- return payload.subarray(offset + 5, frameEnd);
110
- }
111
- offset = frameEnd;
112
- }
113
- return null;
114
- }
115
141
  function normalizeCursorModels(models) {
116
142
  if (models.length === 0)
117
143
  return [];
@@ -0,0 +1,3 @@
1
+ export * from "./messages";
2
+ export * from "./tools";
3
+ export * from "./types";
@@ -0,0 +1,3 @@
1
+ export * from "./messages";
2
+ export * from "./tools";
3
+ export * from "./types";
@@ -0,0 +1,39 @@
1
+ import type { ChatCompletionRequest, OpenAIMessage, OpenAIToolCall } from "./types";
2
+ export interface ToolResultInfo {
3
+ toolCallId: string;
4
+ content: string;
5
+ }
6
+ interface ParsedMessages {
7
+ systemPrompt: string;
8
+ userText: string;
9
+ turns: Array<{
10
+ userText: string;
11
+ assistantText: string;
12
+ }>;
13
+ toolResults: ToolResultInfo[];
14
+ pendingAssistantSummary: string;
15
+ completedTurnsFingerprint: string;
16
+ }
17
+ /** Normalize OpenAI message content to a plain string. */
18
+ export declare function textContent(content: OpenAIMessage["content"]): string;
19
+ export declare function parseMessages(messages: OpenAIMessage[]): ParsedMessages;
20
+ export declare function formatToolCallSummary(call: OpenAIToolCall): string;
21
+ export declare function formatToolResultSummary(result: ToolResultInfo): string;
22
+ export declare function buildCompletedTurnsFingerprint(systemPrompt: string, turns: Array<{
23
+ userText: string;
24
+ assistantText: string;
25
+ }>): string;
26
+ export declare function buildToolResumePrompt(userText: string, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
27
+ export declare function buildInitialHandoffPrompt(userText: string, turns: Array<{
28
+ userText: string;
29
+ assistantText: string;
30
+ }>, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
31
+ export declare function buildTitleSourceText(userText: string, turns: Array<{
32
+ userText: string;
33
+ assistantText: string;
34
+ }>, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
35
+ export declare function detectTitleRequest(body: ChatCompletionRequest): {
36
+ matched: boolean;
37
+ reason: string;
38
+ };
39
+ export {};