@@ -7,7 +7,7 @@ import { translateMcpServers } from "./mcp-config.js";
77import { buildCursorTools } from "./cursor-tools.js" ;
88
99function apiKeyFromAuth ( auth : Auth | undefined ) : string | undefined {
10- return auth ?. type === "api" ? auth . key : undefined ;
10+ return auth ?. type === "api" ? auth . key : undefined ;
1111}
1212
1313/**
@@ -23,117 +23,130 @@ function apiKeyFromAuth(auth: Auth | undefined): string | undefined {
2323 * - `tool.cursor_refresh_models`: force-refresh the model catalog.
2424 */
2525export const CursorPlugin : Plugin = async ( input ) => {
26- // The Cursor API key resolved by opencode's auth loader, captured so the
27- // delegation tools (which don't receive auth directly) can reuse it. Falls
28- // back to the CURSOR_API_KEY env var when the loader hasn't run.
29- let capturedApiKey : string | undefined ;
26+ // The Cursor API key resolved by opencode's auth loader, captured so the
27+ // delegation tools (which don't receive auth directly) can reuse it. Falls
28+ // back to the CURSOR_API_KEY env var when the loader hasn't run.
29+ let capturedApiKey : string | undefined ;
3030
31- return {
32- auth : {
33- provider : PROVIDER_ID ,
34- loader : async ( getAuth ) => {
35- const apiKey = resolveCursorApiKey ( apiKeyFromAuth ( await getAuth ( ) . catch ( ( ) => undefined ) ) ) ;
36- if ( apiKey ) {
37- capturedApiKey = apiKey ;
38- // The `config` hook (which seeds opencode's model picker) runs without
39- // a key. Warm the catalog cache here — the loader is the hook that
40- // reliably has the key — so the next launch seeds the full live
41- // catalog instead of the static fallback. Fire-and-forget: discovery
42- // never throws and must not block auth/provider load.
43- void discoverModels ( { apiKey } ) ;
44- }
45- return apiKey ? { apiKey } : { } ;
46- } ,
47- // A single API-key method. opencode always shows its built-in "Enter your
48- // API key" prompt for `type: "api"`, so we intentionally do NOT declare
49- // custom `prompts` (that asks for the key a second time) or an `authorize`
50- // callback. opencode only passes `authorize` the *custom-prompt* inputs —
51- // never the built-in key — so validating the key in `authorize` would
52- // force that redundant extra prompt. Instead the key is validated on first
53- // use (model discovery / the first call both surface a bad key clearly).
54- methods : [ { type : "api" , label : "Cursor API Key" } ] ,
55- } ,
31+ return {
32+ auth : {
33+ provider : PROVIDER_ID ,
34+ loader : async ( getAuth ) => {
35+ const apiKey = resolveCursorApiKey (
36+ apiKeyFromAuth ( await getAuth ( ) . catch ( ( ) => undefined ) ) ,
37+ ) ;
38+ if ( apiKey ) {
39+ capturedApiKey = apiKey ;
40+ // The `config` hook (which seeds opencode's model picker) runs without
41+ // a key. Warm the catalog cache here — the loader is the hook that
42+ // reliably has the key — so the next launch seeds the full live
43+ // catalog instead of the static fallback. Fire-and-forget: discovery
44+ // never throws and must not block auth/provider load.
45+ void discoverModels ( { apiKey } ) ;
46+ }
47+ return apiKey ? { apiKey } : { } ;
48+ } ,
49+ // A single API-key method. opencode always shows its built-in "Enter your
50+ // API key" prompt for `type: "api"`, so we intentionally do NOT declare
51+ // custom `prompts` (that asks for the key a second time) or an `authorize`
52+ // callback. opencode only passes `authorize` the *custom-prompt* inputs —
53+ // never the built-in key — so validating the key in `authorize` would
54+ // force that redundant extra prompt. Instead the key is validated on first
55+ // use (model discovery / the first call both surface a bad key clearly).
56+ methods : [ { type : "api" , label : "Cursor API Key" } ] ,
57+ } ,
5658
57- config : async ( config ) => {
58- const { models } = await discoverModels ( { } ) ;
59- config . provider ??= { } ;
60- const existing = config . provider [ PROVIDER_ID ] ?? { } ;
61- const existingOptions = ( existing . options ?? { } ) as Record < string , unknown > ;
59+ config : async ( config ) => {
60+ const { models } = await discoverModels ( { } ) ;
61+ config . provider ??= { } ;
62+ const existing = config . provider [ PROVIDER_ID ] ?? { } ;
63+ const existingOptions = ( existing . options ?? { } ) as Record <
64+ string ,
65+ unknown
66+ > ;
6267
63- // Forward opencode's configured MCP servers (e.g. Serena) to the Cursor
64- // agent so it can use the same servers. Opt out via
65- // `provider.cursor.options.forwardMcp: false`.
66- const forwardMcp = existingOptions [ "forwardMcp" ] !== false ;
67- const userMcp = ( existingOptions [ "mcpServers" ] ?? { } ) as Record < string , unknown > ;
68- const mcpServers = forwardMcp
69- ? { ...userMcp , ...translateMcpServers ( config . mcp ) }
70- : userMcp ;
68+ // Forward opencode's configured MCP servers to the Cursor
69+ // agent so it can use the same servers. Opt out via
70+ // `provider.cursor.options.forwardMcp: false`.
71+ const forwardMcp = existingOptions [ "forwardMcp" ] !== false ;
72+ const userMcp = ( existingOptions [ "mcpServers" ] ?? { } ) as Record <
73+ string ,
74+ unknown
75+ > ;
76+ const mcpServers = forwardMcp
77+ ? { ...userMcp , ...translateMcpServers ( config . mcp ) }
78+ : userMcp ;
7179
72- config . provider [ PROVIDER_ID ] = {
73- name : "Cursor" ,
74- npm : providerNpm ( ) ,
75- ...existing ,
76- options : {
77- ...existingOptions ,
78- ...( Object . keys ( mcpServers ) . length > 0 ? { mcpServers } : { } ) ,
79- } ,
80- models : { ...toOpencodeModels ( models ) , ...( existing . models ?? { } ) } ,
81- } ;
82- } ,
80+ config . provider [ PROVIDER_ID ] = {
81+ name : "Cursor" ,
82+ npm : providerNpm ( ) ,
83+ ...existing ,
84+ options : {
85+ ...existingOptions ,
86+ ...( Object . keys ( mcpServers ) . length > 0 ? { mcpServers } : { } ) ,
87+ } ,
88+ models : { ...toOpencodeModels ( models ) , ...( existing . models ?? { } ) } ,
89+ } ;
90+ } ,
8391
84- provider : {
85- id : PROVIDER_ID ,
86- models : async ( _provider , ctx ) => {
87- const apiKey = apiKeyFromAuth ( ctx . auth ) ;
88- const { models } = await discoverModels ( { apiKey } ) ;
89- return buildModelV2Map ( models ) ;
90- } ,
91- } ,
92+ provider : {
93+ id : PROVIDER_ID ,
94+ models : async ( _provider , ctx ) => {
95+ const apiKey = apiKeyFromAuth ( ctx . auth ) ;
96+ const { models } = await discoverModels ( { apiKey } ) ;
97+ return buildModelV2Map ( models ) ;
98+ } ,
99+ } ,
92100
93- // Bridge opencode's session id to the provider: it lands in
94- // providerOptions.cursor.sessionID, which the provider reads to pool/resume a
95- // Cursor agent per session (when the `session` option is enabled).
96- //
97- // Also map opencode's plan AGENT to Cursor's plan mode. This hook fires
98- // after opencode merges the selected variant into `output.options`, so an
99- // explicit mode from the `plan` variant (or model options) wins — the
100- // agent-based default only applies when no mode was set.
101- "chat.params" : async ( input , output ) => {
102- if ( input . model ?. providerID !== PROVIDER_ID ) return ;
103- output . options = { ...( output . options ?? { } ) , sessionID : input . sessionID } ;
104- if ( input . agent === "plan" && output . options [ "mode" ] === undefined ) {
105- output . options [ "mode" ] = "plan" ;
106- }
107- } ,
101+ // Bridge opencode's session id to the provider: it lands in
102+ // providerOptions.cursor.sessionID, which the provider reads to pool/resume a
103+ // Cursor agent per session (when the `session` option is enabled).
104+ //
105+ // Also map opencode's plan AGENT to Cursor's plan mode. This hook fires
106+ // after opencode merges the selected variant into `output.options`, so an
107+ // explicit mode from the `plan` variant (or model options) wins — the
108+ // agent-based default only applies when no mode was set.
109+ "chat.params" : async ( input , output ) => {
110+ if ( input . model ?. providerID !== PROVIDER_ID ) return ;
111+ output . options = {
112+ ...( output . options ?? { } ) ,
113+ sessionID : input . sessionID ,
114+ } ;
115+ if ( input . agent === "plan" && output . options [ "mode" ] === undefined ) {
116+ output . options [ "mode" ] = "plan" ;
117+ }
118+ } ,
108119
109- tool : {
110- cursor_refresh_models : {
111- description :
112- "Refresh the live Cursor model catalog (bypasses the 24h cache) and report the available models." ,
113- args : { } ,
114- execute : async ( ) => {
115- const result = await discoverModels ( { forceRefresh : true } ) ;
116- const lines = result . models . map ( ( m ) => `- ${ m . id } — ${ m . displayName } ` ) ;
117- const header =
118- result . source === "live"
119- ? `Refreshed ${ result . models . length } Cursor models (live):`
120- : `Could not fetch live models (${ result . source } ). ${ result . warning ?? "" } ` . trim ( ) ;
121- return {
122- title : `Cursor models (${ result . source } )` ,
123- output : [ header , ...lines ] . join ( "\n" ) ,
124- metadata : { source : result . source , count : result . models . length } ,
125- } ;
126- } ,
127- } ,
128- // Delegation tools that complement the provider: a cloud/background agent
129- // and a permission-gated local delegate. They resolve the Cursor key from
130- // the auth loader (captured above) or CURSOR_API_KEY.
131- ...buildCursorTools ( {
132- resolveApiKey : ( ) => resolveCursorApiKey ( capturedApiKey ) ,
133- defaultCwd : ( ) => input ?. directory ?? process . cwd ( ) ,
134- } ) ,
135- } ,
136- } ;
120+ tool : {
121+ cursor_refresh_models : {
122+ description :
123+ "Refresh the live Cursor model catalog (bypasses the 24h cache) and report the available models." ,
124+ args : { } ,
125+ execute : async ( ) => {
126+ const result = await discoverModels ( { forceRefresh : true } ) ;
127+ const lines = result . models . map (
128+ ( m ) => `- ${ m . id } — ${ m . displayName } ` ,
129+ ) ;
130+ const header =
131+ result . source === "live"
132+ ? `Refreshed ${ result . models . length } Cursor models (live):`
133+ : `Could not fetch live models (${ result . source } ). ${ result . warning ?? "" } ` . trim ( ) ;
134+ return {
135+ title : `Cursor models (${ result . source } )` ,
136+ output : [ header , ...lines ] . join ( "\n" ) ,
137+ metadata : { source : result . source , count : result . models . length } ,
138+ } ;
139+ } ,
140+ } ,
141+ // Delegation tools that complement the provider: a cloud/background agent
142+ // and a permission-gated local delegate. They resolve the Cursor key from
143+ // the auth loader (captured above) or CURSOR_API_KEY.
144+ ...buildCursorTools ( {
145+ resolveApiKey : ( ) => resolveCursorApiKey ( capturedApiKey ) ,
146+ defaultCwd : ( ) => input ?. directory ?? process . cwd ( ) ,
147+ } ) ,
148+ } ,
149+ } ;
137150} ;
138151
139152export default CursorPlugin ;
0 commit comments