cursor-opencode-provider 0.7.6 → 0.8.1
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/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -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 +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- 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.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- 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.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/model-config.d.ts
CHANGED
|
@@ -8,6 +8,15 @@ import { type ModelInfo, type ModelVariant } from "./models.js";
|
|
|
8
8
|
* tagged redundantly.
|
|
9
9
|
*/
|
|
10
10
|
export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* models.dev-style family for a Cursor model id: the id without its version
|
|
13
|
+
* segments and context/speed suffixes (`claude-haiku-4-5` → `claude-haiku`,
|
|
14
|
+
* `gemini-3.8-flash` → `gemini-flash`, `gpt-5.6-luna` → `gpt-luna`).
|
|
15
|
+
* Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). OpenCode
|
|
16
|
+
* picks its small title model by family and falls back to the session model when
|
|
17
|
+
* no entry has one. Cursor Auto (`default`) is not a model family.
|
|
18
|
+
*/
|
|
19
|
+
export declare function cursorModelFamily(id: string): string | undefined;
|
|
11
20
|
export declare function modelInfoToConfig(mi: ModelInfo, options?: {
|
|
12
21
|
thinkingSuffix?: boolean;
|
|
13
22
|
contextTier?: "base" | "long";
|
package/dist/model-config.js
CHANGED
|
@@ -91,7 +91,7 @@ function modelInfoVariants(mi, variants) {
|
|
|
91
91
|
// variant entry indistinguishable from the model entry in pickers that
|
|
92
92
|
// collapse them.
|
|
93
93
|
if (key === baseName && !usedKeys.has(key)) {
|
|
94
|
-
key = `${baseName}${tagDims(v.parameterValues)
|
|
94
|
+
key = `${baseName}${tagDims(v.parameterValues) || " default"}`;
|
|
95
95
|
}
|
|
96
96
|
else if (usedKeys.has(key)) {
|
|
97
97
|
key = `${sanitized}${tagDims(v.parameterValues)}`;
|
|
@@ -140,6 +140,27 @@ export function thinkingSuffixBaseNames(models) {
|
|
|
140
140
|
ambiguous.add(base);
|
|
141
141
|
return ambiguous;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* models.dev-style family for a Cursor model id: the id without its version
|
|
145
|
+
* segments and context/speed suffixes (`claude-haiku-4-5` → `claude-haiku`,
|
|
146
|
+
* `gemini-3.8-flash` → `gemini-flash`, `gpt-5.6-luna` → `gpt-luna`).
|
|
147
|
+
* Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). OpenCode
|
|
148
|
+
* picks its small title model by family and falls back to the session model when
|
|
149
|
+
* no entry has one. Cursor Auto (`default`) is not a model family.
|
|
150
|
+
*/
|
|
151
|
+
export function cursorModelFamily(id) {
|
|
152
|
+
if (id === "default")
|
|
153
|
+
return undefined;
|
|
154
|
+
const base = id.replace(/(?:-(?:1m|fast))+$/, "");
|
|
155
|
+
const kimi = /^kimi-(k\d+)(?:[.p]\d+)*(?:-code(?:-highspeed)?)?$/.exec(base);
|
|
156
|
+
if (kimi)
|
|
157
|
+
return `kimi-${kimi[1]}`;
|
|
158
|
+
const family = base
|
|
159
|
+
.split("-")
|
|
160
|
+
.filter((part) => !/^\d+(?:[.p]\d+)*$/.test(part))
|
|
161
|
+
.join("-");
|
|
162
|
+
return family || undefined;
|
|
163
|
+
}
|
|
143
164
|
export function modelInfoToConfig(mi, options = {}) {
|
|
144
165
|
const contextTier = options.contextTier ?? "base";
|
|
145
166
|
const variants = options.variants ?? variantsForTier(mi, contextTier);
|
|
@@ -182,6 +203,12 @@ export function modelInfoToConfig(mi, options = {}) {
|
|
|
182
203
|
output,
|
|
183
204
|
},
|
|
184
205
|
};
|
|
206
|
+
// Optional cache metadata may supply a family; the live AvailableModels
|
|
207
|
+
// schema has no family field. Empty metadata must not block derivation.
|
|
208
|
+
const reported = typeof mi.family === "string" ? mi.family.trim() : "";
|
|
209
|
+
const family = reported || cursorModelFamily(mi.id);
|
|
210
|
+
if (family)
|
|
211
|
+
config.family = family;
|
|
185
212
|
const variantConfig = modelInfoVariants(mi, variants);
|
|
186
213
|
if (variantConfig)
|
|
187
214
|
config.variants = variantConfig;
|
|
@@ -69,6 +69,8 @@ export function modelConfigEntryToInfo(id, entry) {
|
|
|
69
69
|
time: { released: 0 },
|
|
70
70
|
cost: toOpenCode2Costs(entry.cost),
|
|
71
71
|
};
|
|
72
|
+
if (typeof entry.family === "string" && entry.family.trim())
|
|
73
|
+
info.family = entry.family.trim();
|
|
72
74
|
if (options)
|
|
73
75
|
info.settings = { ...options };
|
|
74
76
|
return info;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PluginContext, ToolDraft } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register the handle-only image commit tool on the OpenCode 2.0 direct
|
|
4
|
+
* catalog. Catalog `permission: "edit"` hides it when the host agent denies
|
|
5
|
+
* edits. Per-call `ask` is used when the runtime context provides it;
|
|
6
|
+
* otherwise containment still gates the write (opaque `image_id` only).
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerCursorImageSaveTool(draft: ToolDraft, ctx: PluginContext): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { executeCursorImageSave } from "../image-save.js";
|
|
2
|
+
import { cursorImageSaveTool } from "../image-save-tool.js";
|
|
3
|
+
import { CURSOR_IMAGE_SAVE_TOOL } from "../protocol/generate-image.js";
|
|
4
|
+
import { getSessionDirectory } from "../session-directory.js";
|
|
5
|
+
import { hostHasTool, OPENCODE2_DIRECT_TOOL_OPTIONS } from "./todo-tools.js";
|
|
6
|
+
const IMAGE_SAVE_INPUT_SCHEMA = {
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {
|
|
10
|
+
image_id: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Id of the pending Cursor-generated image to save",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ["image_id"],
|
|
16
|
+
};
|
|
17
|
+
const IMAGE_SAVE_OUTPUT_SCHEMA = {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
path: { type: "string" },
|
|
22
|
+
bytes: { type: "number" },
|
|
23
|
+
message: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["path", "bytes", "message"],
|
|
26
|
+
};
|
|
27
|
+
function toolAsk(context) {
|
|
28
|
+
if (!context || typeof context !== "object")
|
|
29
|
+
return undefined;
|
|
30
|
+
const ask = context.ask;
|
|
31
|
+
return typeof ask === "function"
|
|
32
|
+
? ask
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
async function workspaceForSession(ctx, sessionID) {
|
|
36
|
+
const marked = getSessionDirectory(sessionID);
|
|
37
|
+
if (marked)
|
|
38
|
+
return marked;
|
|
39
|
+
try {
|
|
40
|
+
return (await ctx.session.get({ sessionID })).location.directory;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return ctx.location?.directory || process.cwd();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Register the handle-only image commit tool on the OpenCode 2.0 direct
|
|
48
|
+
* catalog. Catalog `permission: "edit"` hides it when the host agent denies
|
|
49
|
+
* edits. Per-call `ask` is used when the runtime context provides it;
|
|
50
|
+
* otherwise containment still gates the write (opaque `image_id` only).
|
|
51
|
+
*/
|
|
52
|
+
export function registerCursorImageSaveTool(draft, ctx) {
|
|
53
|
+
if (hostHasTool(draft, CURSOR_IMAGE_SAVE_TOOL))
|
|
54
|
+
return;
|
|
55
|
+
draft.add({
|
|
56
|
+
name: CURSOR_IMAGE_SAVE_TOOL,
|
|
57
|
+
description: cursorImageSaveTool.description,
|
|
58
|
+
input: IMAGE_SAVE_INPUT_SCHEMA,
|
|
59
|
+
output: IMAGE_SAVE_OUTPUT_SCHEMA,
|
|
60
|
+
options: { ...OPENCODE2_DIRECT_TOOL_OPTIONS, permission: "edit" },
|
|
61
|
+
execute: async (input, context) => {
|
|
62
|
+
const sessionID = context.sessionID;
|
|
63
|
+
if (typeof sessionID !== "string" || !sessionID) {
|
|
64
|
+
throw new Error("OpenCode 2.0 image-save context did not provide a sessionID");
|
|
65
|
+
}
|
|
66
|
+
const directory = await workspaceForSession(ctx, sessionID);
|
|
67
|
+
const result = await executeCursorImageSave({ image_id: input?.image_id }, {
|
|
68
|
+
worktree: directory,
|
|
69
|
+
directory,
|
|
70
|
+
// The public 2.0 ToolContext has no permission prompt; catalog
|
|
71
|
+
// `permission: "edit"` and containment gate the write instead.
|
|
72
|
+
ask: toolAsk(context) ?? null,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
output: {
|
|
76
|
+
path: result.title,
|
|
77
|
+
bytes: result.bytes,
|
|
78
|
+
message: result.output,
|
|
79
|
+
},
|
|
80
|
+
content: result.output,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -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;
|
|
@@ -43,6 +43,7 @@ export type ModelInfo2 = {
|
|
|
43
43
|
modelID: string;
|
|
44
44
|
providerID: string;
|
|
45
45
|
name: string;
|
|
46
|
+
family?: string;
|
|
46
47
|
capabilities: {
|
|
47
48
|
tools: boolean;
|
|
48
49
|
input: readonly string[];
|
|
@@ -415,6 +416,19 @@ export type McpEditor = {
|
|
|
415
416
|
export type McpDomain = {
|
|
416
417
|
readonly transform: Transform<McpEditor>;
|
|
417
418
|
};
|
|
419
|
+
/** Only the fields this plugin reads from a registered skill. */
|
|
420
|
+
export type SkillInfo = {
|
|
421
|
+
readonly id: string;
|
|
422
|
+
readonly path: string;
|
|
423
|
+
};
|
|
424
|
+
export type SkillDomain = {
|
|
425
|
+
readonly list: () => Promise<{
|
|
426
|
+
readonly location: {
|
|
427
|
+
readonly directory: string;
|
|
428
|
+
};
|
|
429
|
+
readonly data: readonly SkillInfo[];
|
|
430
|
+
}>;
|
|
431
|
+
};
|
|
418
432
|
export type PluginLocation = {
|
|
419
433
|
readonly directory: string;
|
|
420
434
|
};
|
|
@@ -429,6 +443,7 @@ export type PluginContext = {
|
|
|
429
443
|
readonly shell?: ShellDomain;
|
|
430
444
|
readonly websearch?: WebSearchDomain;
|
|
431
445
|
readonly mcp?: McpDomain;
|
|
446
|
+
readonly skill?: SkillDomain;
|
|
432
447
|
};
|
|
433
448
|
export type Cleanup = () => Promise<void> | void;
|
|
434
449
|
export type Plugin2 = {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel tool-call step boundary for OpenCode's AI SDK step model.
|
|
3
|
+
*
|
|
4
|
+
* Cursor fires execs as they are generated; CLI demuxes them concurrently and
|
|
5
|
+
* has no step boundary. OpenCode needs one `finish: tool-calls` after every
|
|
6
|
+
* call of the generation. Cursor's `tool_requests_listed` (InteractionUpdate
|
|
7
|
+
* field 27) carries that count. See tasks/plans/parallel-tool-calls-cli-parity.md.
|
|
8
|
+
*/
|
|
9
|
+
/** Idle without a progress frame while holding → close the step (ms). */
|
|
10
|
+
export declare const PARALLEL_STEP_IDLE_MS = 1500;
|
|
11
|
+
export type ParallelStepState = {
|
|
12
|
+
/** From field 27; undefined until Cursor lists the count for this step. */
|
|
13
|
+
listedCount?: number;
|
|
14
|
+
/** Model tool_call_ids seen this step. */
|
|
15
|
+
callIds: Set<string>;
|
|
16
|
+
/** Ids with a final client disposition this step. */
|
|
17
|
+
resolved: Set<string>;
|
|
18
|
+
/** Host tool-call parts emitted this step. */
|
|
19
|
+
emitted: number;
|
|
20
|
+
/** Wall time of the last progress frame (for the liveness guard). */
|
|
21
|
+
lastProgressAt: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function noteToolRequestsListedSeen(): void;
|
|
24
|
+
export declare function hasSeenToolRequestsListed(): boolean;
|
|
25
|
+
/** Test-only: reset the process capability gate. */
|
|
26
|
+
export declare function resetToolRequestsListedSeenForTests(): void;
|
|
27
|
+
export declare function createParallelStep(now?: number): ParallelStepState;
|
|
28
|
+
export declare function noteParallelStepProgress(step: ParallelStepState, now?: number): void;
|
|
29
|
+
export declare function noteListedCallCount(step: ParallelStepState, callCount: number, now?: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Record a final disposition for `callId`. Returns true when newly resolved.
|
|
32
|
+
* Empty ids are ignored. Prior-step ids in `prior` are ignored.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveParallelStepCall(step: ParallelStepState, callId: string | undefined, prior?: ReadonlySet<string>, now?: number): boolean;
|
|
35
|
+
export declare function recordParallelStepEmission(step: ParallelStepState, now?: number): void;
|
|
36
|
+
/** True when the listed count is known and every listed call is resolved. */
|
|
37
|
+
export declare function parallelStepCountMet(step: ParallelStepState): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to keep pumping for more calls of this step instead of finishing.
|
|
40
|
+
*
|
|
41
|
+
* - Gate off and no count yet → do not hold (today's one-call finish).
|
|
42
|
+
* - Human-gated interaction with unknown count → do not hold (prompt must show).
|
|
43
|
+
* - Otherwise hold until the count is met (or the liveness guard fires).
|
|
44
|
+
*/
|
|
45
|
+
export declare function shouldHoldParallelStep(step: ParallelStepState, options?: {
|
|
46
|
+
fieldSeen?: boolean;
|
|
47
|
+
humanGatedWithoutCount?: boolean;
|
|
48
|
+
}): boolean;
|
|
49
|
+
/** True when the hold has made no progress for `idleMs`. */
|
|
50
|
+
export declare function shouldGuardCloseParallelStep(step: ParallelStepState, now?: number, idleMs?: number): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Frames that reset the liveness guard while holding. Heartbeat, KV, and
|
|
53
|
+
* checkpoint are not progress — they say nothing about further tool calls.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isParallelStepProgressFrame(kinds: {
|
|
56
|
+
toolRequestsListed?: boolean;
|
|
57
|
+
partialToolCall?: boolean;
|
|
58
|
+
toolCallDelta?: boolean;
|
|
59
|
+
toolCallStarted?: boolean;
|
|
60
|
+
toolCallCompleted?: boolean;
|
|
61
|
+
exec?: boolean;
|
|
62
|
+
interactionQuery?: boolean;
|
|
63
|
+
textDelta?: boolean;
|
|
64
|
+
thinkingDelta?: boolean;
|
|
65
|
+
}): boolean;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel tool-call step boundary for OpenCode's AI SDK step model.
|
|
3
|
+
*
|
|
4
|
+
* Cursor fires execs as they are generated; CLI demuxes them concurrently and
|
|
5
|
+
* has no step boundary. OpenCode needs one `finish: tool-calls` after every
|
|
6
|
+
* call of the generation. Cursor's `tool_requests_listed` (InteractionUpdate
|
|
7
|
+
* field 27) carries that count. See tasks/plans/parallel-tool-calls-cli-parity.md.
|
|
8
|
+
*/
|
|
9
|
+
/** Idle without a progress frame while holding → close the step (ms). */
|
|
10
|
+
export const PARALLEL_STEP_IDLE_MS = 1_500;
|
|
11
|
+
/** Process-level: Cursor has sent field 27 on any Run in this process. */
|
|
12
|
+
let toolRequestsListedSeen = false;
|
|
13
|
+
export function noteToolRequestsListedSeen() {
|
|
14
|
+
toolRequestsListedSeen = true;
|
|
15
|
+
}
|
|
16
|
+
export function hasSeenToolRequestsListed() {
|
|
17
|
+
return toolRequestsListedSeen;
|
|
18
|
+
}
|
|
19
|
+
/** Test-only: reset the process capability gate. */
|
|
20
|
+
export function resetToolRequestsListedSeenForTests() {
|
|
21
|
+
toolRequestsListedSeen = false;
|
|
22
|
+
}
|
|
23
|
+
export function createParallelStep(now = Date.now()) {
|
|
24
|
+
return {
|
|
25
|
+
callIds: new Set(),
|
|
26
|
+
resolved: new Set(),
|
|
27
|
+
emitted: 0,
|
|
28
|
+
lastProgressAt: now,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function noteParallelStepProgress(step, now = Date.now()) {
|
|
32
|
+
step.lastProgressAt = now;
|
|
33
|
+
}
|
|
34
|
+
export function noteListedCallCount(step, callCount, now = Date.now()) {
|
|
35
|
+
noteToolRequestsListedSeen();
|
|
36
|
+
if (Number.isFinite(callCount) && callCount >= 0) {
|
|
37
|
+
step.listedCount = callCount;
|
|
38
|
+
}
|
|
39
|
+
noteParallelStepProgress(step, now);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Record a final disposition for `callId`. Returns true when newly resolved.
|
|
43
|
+
* Empty ids are ignored. Prior-step ids in `prior` are ignored.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveParallelStepCall(step, callId, prior, now = Date.now()) {
|
|
46
|
+
if (!callId)
|
|
47
|
+
return false;
|
|
48
|
+
if (prior?.has(callId))
|
|
49
|
+
return false;
|
|
50
|
+
step.callIds.add(callId);
|
|
51
|
+
if (step.resolved.has(callId))
|
|
52
|
+
return false;
|
|
53
|
+
step.resolved.add(callId);
|
|
54
|
+
noteParallelStepProgress(step, now);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
export function recordParallelStepEmission(step, now = Date.now()) {
|
|
58
|
+
step.emitted++;
|
|
59
|
+
noteParallelStepProgress(step, now);
|
|
60
|
+
}
|
|
61
|
+
/** True when the listed count is known and every listed call is resolved. */
|
|
62
|
+
export function parallelStepCountMet(step) {
|
|
63
|
+
return step.listedCount !== undefined && step.resolved.size >= step.listedCount;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether to keep pumping for more calls of this step instead of finishing.
|
|
67
|
+
*
|
|
68
|
+
* - Gate off and no count yet → do not hold (today's one-call finish).
|
|
69
|
+
* - Human-gated interaction with unknown count → do not hold (prompt must show).
|
|
70
|
+
* - Otherwise hold until the count is met (or the liveness guard fires).
|
|
71
|
+
*/
|
|
72
|
+
export function shouldHoldParallelStep(step, options = {}) {
|
|
73
|
+
if (options.humanGatedWithoutCount && step.listedCount === undefined)
|
|
74
|
+
return false;
|
|
75
|
+
const fieldSeen = options.fieldSeen ?? hasSeenToolRequestsListed();
|
|
76
|
+
if (!fieldSeen && step.listedCount === undefined)
|
|
77
|
+
return false;
|
|
78
|
+
if (parallelStepCountMet(step))
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** True when the hold has made no progress for `idleMs`. */
|
|
83
|
+
export function shouldGuardCloseParallelStep(step, now = Date.now(), idleMs = PARALLEL_STEP_IDLE_MS) {
|
|
84
|
+
if (parallelStepCountMet(step))
|
|
85
|
+
return false;
|
|
86
|
+
return now - step.lastProgressAt >= idleMs;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Frames that reset the liveness guard while holding. Heartbeat, KV, and
|
|
90
|
+
* checkpoint are not progress — they say nothing about further tool calls.
|
|
91
|
+
*/
|
|
92
|
+
export function isParallelStepProgressFrame(kinds) {
|
|
93
|
+
return !!(kinds.toolRequestsListed
|
|
94
|
+
|| kinds.partialToolCall
|
|
95
|
+
|| kinds.toolCallDelta
|
|
96
|
+
|| kinds.toolCallStarted
|
|
97
|
+
|| kinds.toolCallCompleted
|
|
98
|
+
|| kinds.exec
|
|
99
|
+
|| kinds.interactionQuery
|
|
100
|
+
|| kinds.textDelta
|
|
101
|
+
|| kinds.thinkingDelta);
|
|
102
|
+
}
|