cursor-opencode-provider 0.7.5 → 0.8.0
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 +9 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +42 -5
- package/dist/context/build.js +96 -92
- package/dist/context/dynamic-catalog.d.ts +3 -28
- package/dist/context/dynamic-catalog.js +25 -130
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +37 -9
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +7 -4
- package/dist/context/paths.js +13 -9
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.js +2 -2
- package/dist/image-save.js +3 -3
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +59 -8
- package/dist/language-model.js +458 -118
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/plan-execution-kickoff.js +2 -2
- package/dist/plugin-opencode2.js +10 -16
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +138 -109
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.js +2 -1
- package/dist/protocol/messages.js +22 -4
- package/dist/protocol/request.d.ts +11 -9
- package/dist/protocol/request.js +15 -25
- package/dist/protocol/tools.d.ts +17 -10
- package/dist/protocol/tools.js +53 -93
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/session.d.ts +9 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/package.json +1 -1
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/context/skills.d.ts +0 -11
- package/dist/context/skills.js +0 -110
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type AccessTokenRequest } from "../auth-renewal.js";
|
|
2
|
+
import type { CredentialOAuth, CredentialValue, IntegrationDomain, IntegrationDraft } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Integration registration for the OpenCode 2.0 plugin — the replacement for the
|
|
4
5
|
* classic plugin's `auth` hook (`methods` + `loader`).
|
|
@@ -9,15 +10,37 @@ import type { CredentialValue, IntegrationDomain, IntegrationDraft } from "./typ
|
|
|
9
10
|
export declare const CURSOR_OAUTH_METHOD_ID = "oauth";
|
|
10
11
|
/** Env vars that can supply a Cursor API key without running /connect. */
|
|
11
12
|
export declare const CURSOR_ENV_NAMES: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Host-driven session renewal (`POST /oauth/token`, as Cursor's IDE does).
|
|
15
|
+
* OpenCode 2.0 calls this when the stored `expires` is within five minutes
|
|
16
|
+
* (`packages/core/src/integration.ts`, `connection.resolve`) and persists the
|
|
17
|
+
* result. A session this process already renewed in memory is handed over
|
|
18
|
+
* instead of renewed again. A transient failure returns the unchanged session
|
|
19
|
+
* with `expires` moved to the next allowed attempt, so the host keeps working
|
|
20
|
+
* and asks again later; only a session Cursor ended, or one that expired
|
|
21
|
+
* unrenewed, throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refreshOAuth(credential: CredentialOAuth): Promise<CredentialOAuth>;
|
|
12
24
|
/** Register the Cursor integration and its three connection methods. */
|
|
13
25
|
export declare function applyCursorIntegration(draft: IntegrationDraft): void;
|
|
14
26
|
/**
|
|
15
|
-
* Turn a stored credential into a Cursor access token.
|
|
27
|
+
* Turn a stored credential into a Cursor access token. The two credential
|
|
28
|
+
* kinds are handled separately and never substitute for each other.
|
|
29
|
+
*
|
|
30
|
+
* - `oauth`: the session token, renewed in memory once it is due by Cursor's
|
|
31
|
+
* IDE window (the host itself refreshes only in the last five minutes; it
|
|
32
|
+
* then persists the renewal through `refreshOAuth`).
|
|
33
|
+
* - `key`: a raw `crsr_…` API key (from /connect or `CURSOR_API_KEY`),
|
|
34
|
+
* exchanged for a short-lived JWT and re-exchanged near its expiry. Any
|
|
35
|
+
* other value is an already-issued token and is used as-is.
|
|
16
36
|
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
* Throws `CursorAuthError` when no usable token can be produced.
|
|
38
|
+
*/
|
|
39
|
+
export declare function accessTokenFromCredential(credential: CredentialValue | undefined, request?: AccessTokenRequest): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Current access token of the active Cursor connection, for a Run. Throws a
|
|
42
|
+
* `CursorAuthError` that says why when there is none.
|
|
20
43
|
*/
|
|
21
|
-
export declare function
|
|
22
|
-
/**
|
|
44
|
+
export declare function requireCursorAccessToken(integration: IntegrationDomain, request?: AccessTokenRequest): Promise<string>;
|
|
45
|
+
/** Access token of the active Cursor connection, or undefined (model discovery). */
|
|
23
46
|
export declare function resolveCursorAccessToken(integration: IntegrationDomain): Promise<string | undefined>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CURSOR_WEBSITE_HOST } from "../shared.js";
|
|
2
2
|
import { cursorApiBaseURL } from "../plugin-core.js";
|
|
3
|
-
import { buildLoginUrl, decodeJwtExpiryMs,
|
|
3
|
+
import { buildLoginUrl, decodeJwtExpiryMs, generatePkceChallenge, generatePkceParams, isExchangeableApiKey, pollForTokens, } from "../auth.js";
|
|
4
|
+
import { renewSessionIfDue, resolveApiKeyToken, } from "../auth-renewal.js";
|
|
5
|
+
import { CursorAuthError, CursorProviderError } from "../errors.js";
|
|
6
|
+
import { errorMessage } from "../debug.js";
|
|
4
7
|
import { CURSOR_INTEGRATION_ID } from "./catalog.js";
|
|
5
8
|
/**
|
|
6
9
|
* Integration registration for the OpenCode 2.0 plugin — the replacement for the
|
|
@@ -15,6 +18,13 @@ export const CURSOR_ENV_NAMES = ["CURSOR_API_KEY"];
|
|
|
15
18
|
function websiteURL() {
|
|
16
19
|
return process.env.CURSOR_WEBSITE_URL ?? `https://${CURSOR_WEBSITE_HOST}`;
|
|
17
20
|
}
|
|
21
|
+
/** `expires` of a session credential: the JWT's own expiry, as OpenCode's built-in OAuth integrations store it. */
|
|
22
|
+
function sessionExpires(token) {
|
|
23
|
+
return decodeJwtExpiryMs(token) ?? Date.now();
|
|
24
|
+
}
|
|
25
|
+
function sessionTokensOf(credential) {
|
|
26
|
+
return { accessToken: credential.access, refreshToken: credential.refresh || credential.access };
|
|
27
|
+
}
|
|
18
28
|
/** Browser (PKCE) login: open URL, then poll until Cursor hands back tokens. */
|
|
19
29
|
async function authorizeOAuth() {
|
|
20
30
|
const params = generatePkceParams();
|
|
@@ -30,23 +40,38 @@ async function authorizeOAuth() {
|
|
|
30
40
|
methodID: CURSOR_OAUTH_METHOD_ID,
|
|
31
41
|
access: result.accessToken,
|
|
32
42
|
refresh: result.refreshToken,
|
|
33
|
-
expires:
|
|
43
|
+
expires: sessionExpires(result.accessToken),
|
|
34
44
|
})),
|
|
35
45
|
};
|
|
36
46
|
}
|
|
37
47
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
48
|
+
* Host-driven session renewal (`POST /oauth/token`, as Cursor's IDE does).
|
|
49
|
+
* OpenCode 2.0 calls this when the stored `expires` is within five minutes
|
|
50
|
+
* (`packages/core/src/integration.ts`, `connection.resolve`) and persists the
|
|
51
|
+
* result. A session this process already renewed in memory is handed over
|
|
52
|
+
* instead of renewed again. A transient failure returns the unchanged session
|
|
53
|
+
* with `expires` moved to the next allowed attempt, so the host keeps working
|
|
54
|
+
* and asks again later; only a session Cursor ended, or one that expired
|
|
55
|
+
* unrenewed, throws.
|
|
40
56
|
*/
|
|
41
|
-
async function refreshOAuth(credential) {
|
|
42
|
-
const
|
|
57
|
+
export async function refreshOAuth(credential) {
|
|
58
|
+
const renewal = await renewSessionIfDue(sessionTokensOf(credential), { baseUrl: cursorApiBaseURL() });
|
|
59
|
+
const methodID = credential.methodID || CURSOR_OAUTH_METHOD_ID;
|
|
60
|
+
const metadata = credential.metadata === undefined ? {} : { metadata: credential.metadata };
|
|
61
|
+
if (renewal.renewed) {
|
|
62
|
+
return {
|
|
63
|
+
type: "oauth",
|
|
64
|
+
methodID,
|
|
65
|
+
access: renewal.accessToken,
|
|
66
|
+
refresh: renewal.accessToken,
|
|
67
|
+
expires: sessionExpires(renewal.accessToken),
|
|
68
|
+
...metadata,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
43
71
|
return {
|
|
44
|
-
|
|
45
|
-
methodID
|
|
46
|
-
|
|
47
|
-
refresh: tokens.refreshToken,
|
|
48
|
-
expires: decodeJwtExpiryMs(tokens.accessToken) ?? Date.now(),
|
|
49
|
-
...(credential.metadata === undefined ? {} : { metadata: credential.metadata }),
|
|
72
|
+
...credential,
|
|
73
|
+
methodID,
|
|
74
|
+
expires: renewal.retryAt ?? sessionExpires(credential.access),
|
|
50
75
|
};
|
|
51
76
|
}
|
|
52
77
|
/** Register the Cursor integration and its three connection methods. */
|
|
@@ -75,48 +100,52 @@ export function applyCursorIntegration(draft) {
|
|
|
75
100
|
});
|
|
76
101
|
}
|
|
77
102
|
/**
|
|
78
|
-
* Turn a stored credential into a Cursor access token.
|
|
103
|
+
* Turn a stored credential into a Cursor access token. The two credential
|
|
104
|
+
* kinds are handled separately and never substitute for each other.
|
|
79
105
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
106
|
+
* - `oauth`: the session token, renewed in memory once it is due by Cursor's
|
|
107
|
+
* IDE window (the host itself refreshes only in the last five minutes; it
|
|
108
|
+
* then persists the renewal through `refreshOAuth`).
|
|
109
|
+
* - `key`: a raw `crsr_…` API key (from /connect or `CURSOR_API_KEY`),
|
|
110
|
+
* exchanged for a short-lived JWT and re-exchanged near its expiry. Any
|
|
111
|
+
* other value is an already-issued token and is used as-is.
|
|
112
|
+
*
|
|
113
|
+
* Throws `CursorAuthError` when no usable token can be produced.
|
|
83
114
|
*/
|
|
84
|
-
export async function accessTokenFromCredential(credential) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (credential.access && !isExpiringSoon(credential.access))
|
|
89
|
-
return credential.access;
|
|
90
|
-
if (!credential.refresh)
|
|
91
|
-
return credential.access || undefined;
|
|
92
|
-
try {
|
|
93
|
-
return (await refreshOAuth(credential)).access;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// Fall back to the existing token; the call may still succeed.
|
|
97
|
-
return credential.access || undefined;
|
|
98
|
-
}
|
|
115
|
+
export async function accessTokenFromCredential(credential, request = {}) {
|
|
116
|
+
const force = request.forceRefresh === true;
|
|
117
|
+
if (credential?.type === "oauth") {
|
|
118
|
+
return (await renewSessionIfDue(sessionTokensOf(credential), { baseUrl: cursorApiBaseURL(), force })).accessToken;
|
|
99
119
|
}
|
|
100
|
-
if (credential
|
|
101
|
-
|
|
102
|
-
if (!credential.key.startsWith("crsr_"))
|
|
120
|
+
if (credential?.type === "key") {
|
|
121
|
+
if (!isExchangeableApiKey(credential.key))
|
|
103
122
|
return credential.key;
|
|
104
|
-
|
|
105
|
-
return (await exchangeApiKey(credential.key, cursorApiBaseURL())).accessToken;
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
123
|
+
return (await resolveApiKeyToken(credential.key, { baseUrl: cursorApiBaseURL(), force })).accessToken;
|
|
110
124
|
}
|
|
111
|
-
|
|
125
|
+
throw new CursorAuthError("No Cursor login found; connect Cursor first", { code: "no_credential" });
|
|
112
126
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Current access token of the active Cursor connection, for a Run. Throws a
|
|
129
|
+
* `CursorAuthError` that says why when there is none.
|
|
130
|
+
*/
|
|
131
|
+
export async function requireCursorAccessToken(integration, request = {}) {
|
|
132
|
+
let credential;
|
|
115
133
|
try {
|
|
116
134
|
const connection = await integration.connection.active(CURSOR_INTEGRATION_ID);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
credential = connection ? await integration.connection.resolve(connection) : undefined;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// The host's resolve runs our refreshOAuth; keep its classified error.
|
|
139
|
+
if (error instanceof CursorProviderError)
|
|
140
|
+
throw error;
|
|
141
|
+
throw new CursorAuthError(`Cursor login could not be loaded (${errorMessage(error)})`, { cause: error });
|
|
142
|
+
}
|
|
143
|
+
return accessTokenFromCredential(credential, request);
|
|
144
|
+
}
|
|
145
|
+
/** Access token of the active Cursor connection, or undefined (model discovery). */
|
|
146
|
+
export async function resolveCursorAccessToken(integration) {
|
|
147
|
+
try {
|
|
148
|
+
return await requireCursorAccessToken(integration);
|
|
120
149
|
}
|
|
121
150
|
catch {
|
|
122
151
|
return undefined;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { trace } from "./debug.js";
|
|
14
|
+
import { errorMessage, trace } from "./debug.js";
|
|
15
15
|
/** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */
|
|
16
16
|
export function createPlanExecutionKickoffText(planPath) {
|
|
17
17
|
const where = planPath.trim() || "the plan";
|
|
@@ -149,7 +149,7 @@ export async function flushPlanExecutionKickoff(sessionID, options = {}) {
|
|
|
149
149
|
return true;
|
|
150
150
|
}
|
|
151
151
|
catch (error) {
|
|
152
|
-
const detail =
|
|
152
|
+
const detail = errorMessage(error);
|
|
153
153
|
const message = `The approved plan could not start execution: ${detail}. ` +
|
|
154
154
|
"The plan remains active and can be retried.";
|
|
155
155
|
state.status = "failed";
|
package/dist/plugin-opencode2.js
CHANGED
|
@@ -7,7 +7,7 @@ import { sessionActivity } from "./activity.js";
|
|
|
7
7
|
import { fetchOpenCodeWebSearchText, parseExaWebSearchResults, } from "./web-tools.js";
|
|
8
8
|
import { captureCursorShellResult, cursorShellEnvForCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
|
|
9
9
|
import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js";
|
|
10
|
-
import { applyCursorIntegration, resolveCursorAccessToken } from "./opencode2/integration.js";
|
|
10
|
+
import { applyCursorIntegration, requireCursorAccessToken, resolveCursorAccessToken, } from "./opencode2/integration.js";
|
|
11
11
|
import { exposeDirectMcpTools, rememberDirectMcpNamespaces } from "./opencode2/mcp-direct.js";
|
|
12
12
|
import { registerTodoTools } from "./opencode2/todo-tools.js";
|
|
13
13
|
import { OPENCODE_2_TOOL_DIALECT } from "./protocol/tools.js";
|
|
@@ -140,26 +140,20 @@ const plugin = {
|
|
|
140
140
|
let sourceConnection;
|
|
141
141
|
// ── Credentials ─────────────────────────────────────────
|
|
142
142
|
await track(ctx.integration.transform(applyCursorIntegration));
|
|
143
|
-
let cachedToken;
|
|
144
143
|
let tokenInflight;
|
|
145
144
|
/**
|
|
146
|
-
*
|
|
145
|
+
* Current token for model discovery and endpoint warmup, or undefined.
|
|
147
146
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
147
|
+
* Resolved every time (concurrent callers share one attempt) and never
|
|
148
|
+
* memoized: on a fresh install `setup()` runs before the user has
|
|
149
|
+
* connected, and a long-running daemon outlives any single token. Both
|
|
150
|
+
* the host resolve and the renewal layer are cheap until a renewal is due.
|
|
152
151
|
*/
|
|
153
152
|
const accessToken = async () => {
|
|
154
|
-
if (cachedToken)
|
|
155
|
-
return cachedToken;
|
|
156
153
|
tokenInflight ??= resolveCursorAccessToken(ctx.integration).finally(() => {
|
|
157
154
|
tokenInflight = undefined;
|
|
158
155
|
});
|
|
159
|
-
|
|
160
|
-
if (token)
|
|
161
|
-
cachedToken = token;
|
|
162
|
-
return token;
|
|
156
|
+
return tokenInflight;
|
|
163
157
|
};
|
|
164
158
|
const refreshSourceConnection = async () => {
|
|
165
159
|
try {
|
|
@@ -210,10 +204,11 @@ const plugin = {
|
|
|
210
204
|
return;
|
|
211
205
|
if (!isCursorPackage(event.package, event.model.providerID))
|
|
212
206
|
return;
|
|
213
|
-
const token = await accessToken();
|
|
214
207
|
event.sdk = createSdk({
|
|
215
208
|
name: event.model.providerID || CURSOR_PROVIDER_ID,
|
|
216
|
-
|
|
209
|
+
// Resolved per Run open: this SDK lives as long as the daemon, so
|
|
210
|
+
// a token captured here would outlive its expiry.
|
|
211
|
+
getAccessToken: (request) => requireCursorAccessToken(ctx.integration, request),
|
|
217
212
|
// Static fallback only. This hook fires once per model/package, not
|
|
218
213
|
// per session, and 2.0 runs one daemon across many projects — the
|
|
219
214
|
// real per-request directory comes from `x-opencode-directory`
|
|
@@ -467,7 +462,6 @@ const plugin = {
|
|
|
467
462
|
retry.unref?.();
|
|
468
463
|
void ensureModels().catch(() => { });
|
|
469
464
|
const onCredentialSwitch = () => {
|
|
470
|
-
cachedToken = undefined;
|
|
471
465
|
modelsLoaded = false;
|
|
472
466
|
credentialGeneration++;
|
|
473
467
|
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
|
-
export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js";
|
|
3
|
-
export declare function loadClassicTools(options?: {
|
|
4
|
-
importModule?: (specifier: string) => Promise<{
|
|
5
|
-
tool?: any;
|
|
6
|
-
}>;
|
|
7
|
-
configDirs?: string[];
|
|
8
|
-
}): Promise<{
|
|
9
|
-
webSearch: any;
|
|
10
|
-
imageSave: any;
|
|
11
|
-
}>;
|
|
12
2
|
export declare function CursorPlugin(input: PluginInput): Promise<Hooks>;
|
package/dist/plugin.js
CHANGED
|
@@ -1,57 +1,45 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
1
|
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, } from "./shared.js";
|
|
4
2
|
import { cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled } from "./plugin-core.js";
|
|
5
|
-
import { pollForTokens, exchangeApiKey,
|
|
3
|
+
import { pollForTokens, exchangeApiKey, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs, isExchangeableApiKey } from "./auth.js";
|
|
4
|
+
import { renewSessionIfDue, resolveApiKeyToken, } from "./auth-renewal.js";
|
|
5
|
+
import { CursorAuthError } from "./errors.js";
|
|
6
|
+
import { errorMessage, trace } from "./debug.js";
|
|
6
7
|
import { readCache, discoverModels, isCacheFresh } from "./models.js";
|
|
7
|
-
// Re-exported for API compatibility: tests and downstream code import these
|
|
8
|
-
// from "./plugin.js". Canonical home is ./model-config.ts, which stays free of
|
|
9
|
-
// host plugin imports so the OpenCode 2.0 entrypoint can share it.
|
|
10
8
|
import { modelsToConfig } from "./model-config.js";
|
|
11
|
-
|
|
12
|
-
import { opencodeGlobalCacheDir
|
|
9
|
+
import { loadClassicTools } from "./classic-tools.js";
|
|
10
|
+
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
13
11
|
import { readStoredAuth } from "./context/auth-store.js";
|
|
14
12
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
15
13
|
import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js";
|
|
16
14
|
import { sessionActivity } from "./activity.js";
|
|
17
|
-
import { createOpenCodeWebSearchTool, openCodeWebSearchTool } from "./web-search-tool.js";
|
|
18
|
-
import { createCursorImageSaveTool, cursorImageSaveTool } from "./image-save-tool.js";
|
|
19
15
|
import { createPlanExecutionKickoffText, setPlanExecutionKickoff, } from "./plan-execution-kickoff.js";
|
|
20
16
|
import { dispatchHostEventBridge } from "./host-event-bridge.js";
|
|
21
17
|
const MODULE_URL = new URL("./index.js", import.meta.url).href;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
webSearch: createOpenCodeWebSearchTool(factory),
|
|
44
|
-
imageSave: createCursorImageSaveTool(factory),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// Try the next host-owned/normal resolution location.
|
|
50
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* Raw `crsr_` key behind an API-key login: under `metadata.apiKey` (saved at
|
|
20
|
+
* login, and merged there from the prompt inputs by OpenCode's CLI), or as
|
|
21
|
+
* `key` itself when OpenCode stored the typed key without our exchange.
|
|
22
|
+
*/
|
|
23
|
+
function storedApiKey(auth) {
|
|
24
|
+
const fromMetadata = auth.metadata?.apiKey;
|
|
25
|
+
if (typeof fromMetadata === "string" && isExchangeableApiKey(fromMetadata))
|
|
26
|
+
return fromMetadata;
|
|
27
|
+
return isExchangeableApiKey(auth.key) ? auth.key : undefined;
|
|
28
|
+
}
|
|
29
|
+
function sessionTokens(auth) {
|
|
30
|
+
// Cursor's IDE refreshes with the stored refresh token and then stores the
|
|
31
|
+
// new access token as both; an empty refresh field means the same.
|
|
32
|
+
return { accessToken: auth.access, refreshToken: auth.refresh || auth.access };
|
|
33
|
+
}
|
|
34
|
+
/** Whether `latest` is still the credential a renewal started from. */
|
|
35
|
+
function isSameCredential(latest, started) {
|
|
36
|
+
if (latest.type === "oauth" && started.type === "oauth") {
|
|
37
|
+
return latest.access === started.access && latest.refresh === started.refresh;
|
|
51
38
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
if (latest.type === "api" && started.type === "api") {
|
|
40
|
+
return latest.key === started.key && storedApiKey(latest) === storedApiKey(started);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
55
43
|
}
|
|
56
44
|
export async function CursorPlugin(input) {
|
|
57
45
|
const cacheDir = opencodeGlobalCacheDir();
|
|
@@ -76,13 +64,7 @@ export async function CursorPlugin(input) {
|
|
|
76
64
|
});
|
|
77
65
|
}
|
|
78
66
|
: undefined);
|
|
79
|
-
|
|
80
|
-
// loadModels can only read OpenCode's durable store (auth.json /
|
|
81
|
-
// OPENCODE_AUTH_CONTENT); auth.loader gets live credentials via getAuth().
|
|
82
|
-
// Those usually match, but after a refresh where persistAuthBestEffort fails,
|
|
83
|
-
// getAuth() may still see the old credentials while we already hold a usable
|
|
84
|
-
// token here — keep it so the loader can still discover models.
|
|
85
|
-
let sessionAccessToken;
|
|
67
|
+
let lastPersistAttempt;
|
|
86
68
|
async function persistAuth(body) {
|
|
87
69
|
await input.client.auth.set({
|
|
88
70
|
path: { id: CURSOR_PROVIDER_ID },
|
|
@@ -110,64 +92,93 @@ export async function CursorPlugin(input) {
|
|
|
110
92
|
* and config share the same underlying credentials when possible.
|
|
111
93
|
*/
|
|
112
94
|
async function authForLoader(getAuth) {
|
|
113
|
-
|
|
95
|
+
// getAuth is also called long after the loader returned (per-Run token
|
|
96
|
+
// resolution); never let a host-side failure there hide the durable store.
|
|
97
|
+
return (await getAuth().catch(() => undefined)) ?? (await authFromStore());
|
|
114
98
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// refresh failed — keep the existing key; the next call may still work
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (accessToken)
|
|
139
|
-
sessionAccessToken = accessToken;
|
|
140
|
-
return accessToken;
|
|
99
|
+
/**
|
|
100
|
+
* Persist a renewed credential unless the stored one changed meanwhile (a
|
|
101
|
+
* re-login, or another process's renewal): never overwrite newer state.
|
|
102
|
+
*/
|
|
103
|
+
async function persistRenewal(started, next, readCurrent) {
|
|
104
|
+
// One attempt per renewed token: when the write fails (read-only store,
|
|
105
|
+
// injected OPENCODE_AUTH_CONTENT) later Runs keep the in-memory token
|
|
106
|
+
// instead of re-reading and re-writing the store on every turn.
|
|
107
|
+
const token = next.type === "oauth" ? next.access : next.type === "api" ? next.key : undefined;
|
|
108
|
+
if (token === undefined || token === lastPersistAttempt)
|
|
109
|
+
return;
|
|
110
|
+
lastPersistAttempt = token;
|
|
111
|
+
const latest = await readCurrent().catch(() => undefined);
|
|
112
|
+
if (!latest || !isSameCredential(latest, started)) {
|
|
113
|
+
trace("auth: stored Cursor credential changed during renewal; keeping the stored one");
|
|
114
|
+
return;
|
|
141
115
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
116
|
+
await persistAuthBestEffort(next);
|
|
117
|
+
}
|
|
118
|
+
/** Browser-login session: renew when due (or forced) and persist the result. */
|
|
119
|
+
async function resolveSession(auth, readCurrent, force = false) {
|
|
120
|
+
const renewal = await renewSessionIfDue(sessionTokens(auth), { baseUrl: apiBaseURL, force });
|
|
121
|
+
if (renewal.renewed) {
|
|
122
|
+
// Preserve optional OAuth fields (v2 Auth / plugin may carry these).
|
|
123
|
+
const extras = auth;
|
|
124
|
+
await persistRenewal(auth, {
|
|
125
|
+
type: "oauth",
|
|
126
|
+
access: renewal.accessToken,
|
|
127
|
+
refresh: renewal.accessToken,
|
|
128
|
+
expires: decodeJwtExpiryMs(renewal.accessToken) ?? Date.now(),
|
|
129
|
+
...(extras.accountId !== undefined ? { accountId: extras.accountId } : {}),
|
|
130
|
+
...(extras.enterpriseUrl !== undefined ? { enterpriseUrl: extras.enterpriseUrl } : {}),
|
|
131
|
+
}, readCurrent);
|
|
132
|
+
}
|
|
133
|
+
return renewal;
|
|
134
|
+
}
|
|
135
|
+
/** API-key login: re-exchange the stored raw key when the JWT nears expiry. */
|
|
136
|
+
async function resolveApiKeyLogin(auth, readCurrent, force = false) {
|
|
137
|
+
const apiKey = storedApiKey(auth);
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
// Saved by an older version that kept only the exchanged JWT and its
|
|
140
|
+
// refresh token. Cursor renews API-key logins only by exchanging the key.
|
|
141
|
+
if (!isExpiringSoon(auth.key, 30))
|
|
142
|
+
return auth.key;
|
|
143
|
+
throw new CursorAuthError("This Cursor API-key login was saved without the key, so it cannot be renewed; sign in again with the API key", { code: "api_key_missing" });
|
|
144
|
+
}
|
|
145
|
+
const token = await resolveApiKeyToken(apiKey, {
|
|
146
|
+
baseUrl: apiBaseURL,
|
|
147
|
+
...(isExchangeableApiKey(auth.key) ? {} : { seed: auth.key }),
|
|
148
|
+
force,
|
|
149
|
+
});
|
|
150
|
+
if (token.renewed) {
|
|
151
|
+
const { refreshToken: _unused, ...metadata } = auth.metadata ?? {};
|
|
152
|
+
await persistRenewal(auth, {
|
|
153
|
+
type: "api",
|
|
154
|
+
key: token.accessToken,
|
|
155
|
+
metadata: { ...metadata, apiKey },
|
|
156
|
+
}, readCurrent);
|
|
168
157
|
}
|
|
158
|
+
return token.accessToken;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Current access token for the stored credential. The two credential kinds
|
|
162
|
+
* are handled by separate paths and never substitute for each other.
|
|
163
|
+
*/
|
|
164
|
+
async function resolveAccessToken(auth, readCurrent, force = false) {
|
|
165
|
+
if (auth.type === "oauth")
|
|
166
|
+
return (await resolveSession(auth, readCurrent, force)).accessToken;
|
|
167
|
+
if (auth.type === "api")
|
|
168
|
+
return resolveApiKeyLogin(auth, readCurrent, force);
|
|
169
169
|
return undefined;
|
|
170
170
|
}
|
|
171
|
+
/** Best-effort token for startup work (model discovery, endpoint warmup). */
|
|
172
|
+
async function startupAccessToken(auth, readCurrent) {
|
|
173
|
+
try {
|
|
174
|
+
return await resolveAccessToken(auth, readCurrent);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// Surfaced again, with the same message, when a Run asks for a token.
|
|
178
|
+
trace(`auth: no usable Cursor token at startup (${errorMessage(error)})`);
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
171
182
|
async function loadModels() {
|
|
172
183
|
const cached = await readCache(cacheDir);
|
|
173
184
|
if (cached?.models.length && isCacheFresh(cached)) {
|
|
@@ -178,7 +189,7 @@ export async function CursorPlugin(input) {
|
|
|
178
189
|
// or old-schema caches here so this process materializes the new model set.
|
|
179
190
|
const auth = await authFromStore();
|
|
180
191
|
if (auth) {
|
|
181
|
-
const accessToken = await
|
|
192
|
+
const accessToken = await startupAccessToken(auth, authFromStore);
|
|
182
193
|
if (accessToken) {
|
|
183
194
|
try {
|
|
184
195
|
const models = await discoverModels(accessToken, cacheDir, { baseURL: apiBaseURL });
|
|
@@ -352,7 +363,9 @@ export async function CursorPlugin(input) {
|
|
|
352
363
|
type: "success",
|
|
353
364
|
key: result.accessToken,
|
|
354
365
|
provider: CURSOR_PROVIDER_ID,
|
|
355
|
-
|
|
366
|
+
// Keep the raw key: Cursor renews an API-key login only by
|
|
367
|
+
// exchanging the key again (its refresh token is never used).
|
|
368
|
+
metadata: { apiKey },
|
|
356
369
|
};
|
|
357
370
|
}
|
|
358
371
|
catch {
|
|
@@ -362,10 +375,11 @@ export async function CursorPlugin(input) {
|
|
|
362
375
|
},
|
|
363
376
|
],
|
|
364
377
|
async loader(getAuth) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
|
|
378
|
+
const readCurrent = () => authForLoader(getAuth);
|
|
379
|
+
const auth = await readCurrent();
|
|
380
|
+
// Model discovery and endpoint warmup need a token: like any request,
|
|
381
|
+
// that renews a due session (or expiring API-key JWT) first.
|
|
382
|
+
const accessToken = auth ? await startupAccessToken(auth, readCurrent) : undefined;
|
|
369
383
|
if (accessToken) {
|
|
370
384
|
// Skip when config already filled a fresh cache (avoids a second
|
|
371
385
|
// AvailableModels round-trip + background refresh on cold start).
|
|
@@ -384,8 +398,23 @@ export async function CursorPlugin(input) {
|
|
|
384
398
|
telemetryEnabled: cursorGetServerConfigTelemetryEnabled(),
|
|
385
399
|
}).catch(() => { });
|
|
386
400
|
}
|
|
401
|
+
// Asked for on every Run open: reads the live credential (so a
|
|
402
|
+
// re-login applies without a restart), renews it when due, persists the
|
|
403
|
+
// renewal. This is how OpenCode's own OAuth providers hand over
|
|
404
|
+
// credentials (codex, xai, copilot: a `fetch` that calls getAuth() per
|
|
405
|
+
// request): no token is placed in the options, which OpenCode serves
|
|
406
|
+
// unredacted from /provider; a function is dropped there.
|
|
407
|
+
const getAccessToken = async (request) => {
|
|
408
|
+
const current = await readCurrent();
|
|
409
|
+
const token = current
|
|
410
|
+
? await resolveAccessToken(current, readCurrent, request?.forceRefresh === true)
|
|
411
|
+
: undefined;
|
|
412
|
+
if (!token)
|
|
413
|
+
throw new CursorAuthError("No Cursor login found; sign in to Cursor", { code: "no_credential" });
|
|
414
|
+
return token;
|
|
415
|
+
};
|
|
387
416
|
return {
|
|
388
|
-
|
|
417
|
+
getAccessToken,
|
|
389
418
|
workspaceRoot: input.directory,
|
|
390
419
|
cacheDir,
|
|
391
420
|
};
|