cursor-opencode-provider 0.7.4 → 0.7.6
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 +2 -0
- package/dist/context/build.d.ts +6 -0
- package/dist/context/build.js +4 -2
- package/dist/context/dynamic-catalog.d.ts +74 -0
- package/dist/context/dynamic-catalog.js +228 -0
- package/dist/context/frozen.js +5 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/context/overlay.d.ts +5 -0
- package/dist/context/overlay.js +7 -0
- package/dist/context/rules.d.ts +2 -1
- package/dist/context/rules.js +3 -2
- package/dist/image-save.d.ts +1 -1
- package/dist/image-save.js +3 -3
- package/dist/image-staging.js +6 -1
- package/dist/language-model.d.ts +23 -1
- package/dist/language-model.js +140 -22
- package/dist/opencode2/mcp-direct.d.ts +7 -2
- package/dist/opencode2/mcp-direct.js +7 -2
- package/dist/opencode2/types.d.ts +25 -1
- package/dist/plugin-opencode2.js +36 -10
- package/dist/pricing-data.d.ts +35 -0
- package/dist/pricing-data.js +35 -0
- package/dist/protocol/generate-image.d.ts +2 -2
- package/dist/protocol/generate-image.js +14 -2
- package/dist/protocol/tools.d.ts +4 -0
- package/dist/protocol/tools.js +30 -1
- package/dist/session-directory.d.ts +6 -3
- package/dist/session-directory.js +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -293,6 +293,8 @@ OpenCode
|
|
|
293
293
|
|
|
294
294
|
The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
|
|
295
295
|
|
|
296
|
+
Cursor keeps OpenCode `skill` and configured MCP tools off its native top-level function list on **both** OpenCode 1.x and OpenCode 2.0 — they appear as MCP descriptors and are reached through GetDynamicTools / CallDynamicTool. When the host advertises `skill` and/or tools of MCP servers configured in `opencode.json`, the provider names those routes in system guidance and mirrors OpenCode's load-when-matches / do-not-reinvoke rules. Mid-Conversation skill reminders fire only when the available-skills catalog changes after baseline admission (same contract as OpenCode `SkillGuidance` / `SkillInstructions`) — not on every user turn that happens to mention a skill id — so models prefer `skill` / MCP over Grep/Shell and do not narrate that those tools are unavailable. OpenCode 2 still moves MCP servers that did not set `"codemode": true` onto the direct AI SDK catalog first; without that placement they never reach Cursor's MCP ads at all (see [OpenCode 2 MCP tools](docs/opencode-2.md#mcp-tools)).
|
|
297
|
+
|
|
296
298
|
Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see `tasks/lessons.md`).
|
|
297
299
|
|
|
298
300
|
If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
|
package/dist/context/build.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { type OpencodeToolDef } from "../protocol/tools.js";
|
|
2
|
+
import { type OpencodeJson } from "./rules.js";
|
|
2
3
|
export type BuildRequestContextInput = {
|
|
3
4
|
workspaceRoot: string;
|
|
4
5
|
tools?: OpencodeToolDef[];
|
|
5
6
|
providerIdentifier?: string;
|
|
6
7
|
/** When set, skills/subagents/plugins are epoch-held for this conversation. */
|
|
7
8
|
conversationId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Preloaded merged `opencode.json` from the same Run (e.g. for interaction
|
|
11
|
+
* guidance MCP server ids). Skips a second `loadMergedConfig` disk read.
|
|
12
|
+
*/
|
|
13
|
+
mergedConfig?: OpencodeJson;
|
|
8
14
|
};
|
|
9
15
|
export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "agent_skills", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "agent_skills_info_complete", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
|
|
10
16
|
export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
|
package/dist/context/build.js
CHANGED
|
@@ -67,7 +67,7 @@ function buildAdvertisedSubagentCatalog(hostSubagents, discoveredAgents) {
|
|
|
67
67
|
*/
|
|
68
68
|
export async function buildRequestContext(input) {
|
|
69
69
|
const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
|
|
70
|
-
const { rules, config, worktree } = await collectRules(workspaceRoot);
|
|
70
|
+
const { rules, config, worktree } = await collectRules(workspaceRoot, input.mergedConfig);
|
|
71
71
|
const [dynamic, git, layout] = await Promise.all([
|
|
72
72
|
buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config),
|
|
73
73
|
collectGit(workspaceRoot),
|
|
@@ -176,7 +176,9 @@ export async function buildDynamicRequestContext(input) {
|
|
|
176
176
|
const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
|
|
177
177
|
const [worktree, config] = await Promise.all([
|
|
178
178
|
findGitWorktree(workspaceRoot),
|
|
179
|
-
|
|
179
|
+
input.mergedConfig
|
|
180
|
+
? Promise.resolve(input.mergedConfig)
|
|
181
|
+
: loadMergedConfig(workspaceRoot),
|
|
180
182
|
]);
|
|
181
183
|
return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config);
|
|
182
184
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type AgentSkillLike = {
|
|
2
|
+
/** OpenCode skill id (frontmatter `name`, else directory name). */
|
|
3
|
+
id?: unknown;
|
|
4
|
+
full_path?: unknown;
|
|
5
|
+
description?: unknown;
|
|
6
|
+
content?: unknown;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Configured MCP servers that own at least one advertised tool. Uses the same
|
|
10
|
+
* server resolution as the RequestContext descriptors, so guidance never names
|
|
11
|
+
* a server Cursor was not given. `toolNames` are OpenCode ids (alias
|
|
12
|
+
* `sourceName` when present).
|
|
13
|
+
*/
|
|
14
|
+
export declare function listAdvertisedMcpServers(toolNames: Iterable<string>, knownMcpServers?: Iterable<string>): string[];
|
|
15
|
+
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
16
|
+
export declare function skillNameFromAgentSkill(skill: AgentSkillLike): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
19
|
+
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
20
|
+
* stay out of the frozen baseline (they live in RequestContext `agent_skills`
|
|
21
|
+
* and in Mid-Conversation updates when the catalog changes — same split as
|
|
22
|
+
* OpenCode's SkillGuidance / SkillInstructions).
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildDynamicCatalogRoutingInstruction(options: {
|
|
25
|
+
toolNames: Iterable<string>;
|
|
26
|
+
knownMcpServers?: Iterable<string>;
|
|
27
|
+
}): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* OpenCode-shaped Mid-Conversation skill update.
|
|
30
|
+
*
|
|
31
|
+
* Pure: compare `previousSkillIds` (null = not yet admitted this conversation)
|
|
32
|
+
* to the current catalog. First admission returns undefined — baseline
|
|
33
|
+
* RequestContext + routing instruction already carry the list. Later turns
|
|
34
|
+
* only speak when the id set changes (agent switch / discovery growth), matching
|
|
35
|
+
* `SkillGuidance.update` / `SkillInstructions.update`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildSkillCatalogChangeReminder(options: {
|
|
38
|
+
hasSkillTool: boolean;
|
|
39
|
+
skills: readonly AgentSkillLike[];
|
|
40
|
+
/** Sorted skill ids already admitted; `null` means first admission this conversation. */
|
|
41
|
+
previousSkillIds: readonly string[] | null;
|
|
42
|
+
/**
|
|
43
|
+
* Host skill parameter key from the advertised schema.
|
|
44
|
+
* OpenCode 1.x: `name`. OpenCode 2.0: `id`.
|
|
45
|
+
*/
|
|
46
|
+
skillArgKey?: "name" | "id";
|
|
47
|
+
}): {
|
|
48
|
+
text: string | undefined;
|
|
49
|
+
nextSkillIds: string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Admit the current skill catalog for a conversation and return a Mid-Conversation
|
|
53
|
+
* reminder only when OpenCode would (catalog changed after baseline admission).
|
|
54
|
+
*/
|
|
55
|
+
export declare function takeSkillCatalogChangeReminder(conversationId: string, options: {
|
|
56
|
+
hasSkillTool: boolean;
|
|
57
|
+
skills: readonly AgentSkillLike[];
|
|
58
|
+
skillArgKey?: "name" | "id";
|
|
59
|
+
}): string | undefined;
|
|
60
|
+
export declare function clearSkillCatalogAdmission(conversationId: string): void;
|
|
61
|
+
export declare function transferSkillCatalogAdmission(previousConversationId: string, nextConversationId: string): void;
|
|
62
|
+
export declare function resetSkillCatalogAdmissionsForTests(): void;
|
|
63
|
+
/**
|
|
64
|
+
* @deprecated Prefer `buildSkillCatalogChangeReminder` / `takeSkillCatalogChangeReminder`.
|
|
65
|
+
* Kept as a thin alias for callers that still expect the old name; always returns
|
|
66
|
+
* undefined for userText-based matching (OpenCode never did per-turn id nudges).
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildSkillCatalogNudge(options: {
|
|
69
|
+
hasSkillTool: boolean;
|
|
70
|
+
skills: readonly AgentSkillLike[];
|
|
71
|
+
userText?: string;
|
|
72
|
+
skillArgKey?: "name" | "id";
|
|
73
|
+
previousSkillIds?: readonly string[] | null;
|
|
74
|
+
}): string | undefined;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { resolveToolServerIdentity } from "../protocol/tools.js";
|
|
2
|
+
/**
|
|
3
|
+
* Issue #29: Cursor keeps OpenCode `skill` and MCP tools off its native
|
|
4
|
+
* top-level function list. Both OpenCode 1.x and OpenCode 2.0 reach the model
|
|
5
|
+
* as MCP-shaped RequestContext ads, then GetDynamicTools / CallDynamicTool
|
|
6
|
+
* (exec `mcp_args`). This module builds the shared routing guidance and the
|
|
7
|
+
* OpenCode-shaped Mid-Conversation skill-catalog update used on both hosts.
|
|
8
|
+
*
|
|
9
|
+
* OpenCode itself does **not** per-turn nudge matched skill ids. It:
|
|
10
|
+
* - Puts name/description guidance in the frozen baseline (`SystemPrompt.skills`
|
|
11
|
+
* / `SkillGuidance` / `SkillInstructions`)
|
|
12
|
+
* - Emits a Mid-Conversation update only when the available-skills list changes
|
|
13
|
+
* - OpenCode 2: skips re-invoke when `<skill_content>` is already in the turn
|
|
14
|
+
*
|
|
15
|
+
* OpenCode 2 still needs `exposeDirectMcpTools` so MCP tools leave Code Mode
|
|
16
|
+
* and enter the AI SDK catalog; without that step they never reach this
|
|
17
|
+
* advertisement path at all.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_MCP_SERVERS_IN_GUIDANCE = 8;
|
|
20
|
+
/** Synthetic server builtins and unknown tools are advertised under (`toolsToMcpDescriptors`). */
|
|
21
|
+
const DEFAULT_TOOL_SERVER = "opencode";
|
|
22
|
+
/**
|
|
23
|
+
* Skill ids already admitted to the model for a conversation (baseline RequestContext
|
|
24
|
+
* freeze, or the last Mid-Conversation catalog update). First admission is silent —
|
|
25
|
+
* OpenCode puts that list in the system baseline, not a per-turn reminder.
|
|
26
|
+
*/
|
|
27
|
+
const admittedSkillIdsByConversation = new Map();
|
|
28
|
+
function listWithOverflow(items, limit) {
|
|
29
|
+
const shown = items.slice(0, limit).map((item) => `\`${item}\``).join(", ");
|
|
30
|
+
const hidden = items.length - limit;
|
|
31
|
+
return hidden > 0 ? `${shown} (+${hidden} more)` : shown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Configured MCP servers that own at least one advertised tool. Uses the same
|
|
35
|
+
* server resolution as the RequestContext descriptors, so guidance never names
|
|
36
|
+
* a server Cursor was not given. `toolNames` are OpenCode ids (alias
|
|
37
|
+
* `sourceName` when present).
|
|
38
|
+
*/
|
|
39
|
+
export function listAdvertisedMcpServers(toolNames, knownMcpServers = []) {
|
|
40
|
+
const known = [...knownMcpServers];
|
|
41
|
+
if (known.length === 0)
|
|
42
|
+
return [];
|
|
43
|
+
const servers = new Set();
|
|
44
|
+
for (const name of toolNames) {
|
|
45
|
+
const { server } = resolveToolServerIdentity(name, DEFAULT_TOOL_SERVER, known);
|
|
46
|
+
if (server !== DEFAULT_TOOL_SERVER)
|
|
47
|
+
servers.add(server);
|
|
48
|
+
}
|
|
49
|
+
return [...servers];
|
|
50
|
+
}
|
|
51
|
+
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
52
|
+
export function skillNameFromAgentSkill(skill) {
|
|
53
|
+
const id = typeof skill.id === "string" ? skill.id.trim() : "";
|
|
54
|
+
if (id)
|
|
55
|
+
return id;
|
|
56
|
+
const fullPath = typeof skill.full_path === "string" ? skill.full_path.trim() : "";
|
|
57
|
+
const parts = fullPath.split(/[\\/]+/).filter(Boolean);
|
|
58
|
+
if (parts.length < 2 || parts.at(-1).toLowerCase() !== "skill.md")
|
|
59
|
+
return undefined;
|
|
60
|
+
return parts.at(-2);
|
|
61
|
+
}
|
|
62
|
+
function skillIds(skills) {
|
|
63
|
+
const ids = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const skill of skills) {
|
|
66
|
+
const name = skillNameFromAgentSkill(skill);
|
|
67
|
+
if (!name || seen.has(name))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(name);
|
|
70
|
+
ids.push(name);
|
|
71
|
+
}
|
|
72
|
+
return [...ids].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
73
|
+
}
|
|
74
|
+
function sameIdList(left, right) {
|
|
75
|
+
if (left.length !== right.length)
|
|
76
|
+
return false;
|
|
77
|
+
for (let i = 0; i < left.length; i++)
|
|
78
|
+
if (left[i] !== right[i])
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
84
|
+
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
85
|
+
* stay out of the frozen baseline (they live in RequestContext `agent_skills`
|
|
86
|
+
* and in Mid-Conversation updates when the catalog changes — same split as
|
|
87
|
+
* OpenCode's SkillGuidance / SkillInstructions).
|
|
88
|
+
*/
|
|
89
|
+
export function buildDynamicCatalogRoutingInstruction(options) {
|
|
90
|
+
const names = [...options.toolNames];
|
|
91
|
+
const hasSkill = names.includes("skill");
|
|
92
|
+
const mcpServers = listAdvertisedMcpServers(names, options.knownMcpServers);
|
|
93
|
+
if (!hasSkill && mcpServers.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const extras = [];
|
|
96
|
+
if (hasSkill)
|
|
97
|
+
extras.push("`skill`");
|
|
98
|
+
if (mcpServers.length > 0) {
|
|
99
|
+
extras.push(`MCP servers such as ${listWithOverflow(mcpServers, MAX_MCP_SERVERS_IN_GUIDANCE)}`);
|
|
100
|
+
}
|
|
101
|
+
const lines = [
|
|
102
|
+
`- OpenCode host tools that are not in Cursor's native top-level list (including ${extras.join(" and ")}) ` +
|
|
103
|
+
"are reached through GetDynamicTools / CallDynamicTool (or the host's equivalent dynamic catalog). " +
|
|
104
|
+
"When a skill matches or project rules name an MCP server, discover and call those tools that way " +
|
|
105
|
+
"before Grep/Shell fallbacks. Do not narrate that they are unavailable.",
|
|
106
|
+
];
|
|
107
|
+
if (hasSkill) {
|
|
108
|
+
// Mirror OpenCode 1 SystemPrompt.skills / OC2 SkillInstructions.render.
|
|
109
|
+
lines.push("- Skills provide specialized instructions and workflows for specific tasks. " +
|
|
110
|
+
"Use the `skill` tool to load a skill when a task matches its description " +
|
|
111
|
+
"(RequestContext `agent_skills` lists names and descriptions). " +
|
|
112
|
+
"A skill that is already present in the conversation as a `<skill_content>` block " +
|
|
113
|
+
"does not need to be invoked again.");
|
|
114
|
+
}
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
function skillEntriesXml(skills, argKey) {
|
|
118
|
+
const lines = ["<available_skills>"];
|
|
119
|
+
for (const skill of skills) {
|
|
120
|
+
const name = skillNameFromAgentSkill(skill);
|
|
121
|
+
if (!name)
|
|
122
|
+
continue;
|
|
123
|
+
const description = typeof skill.description === "string" ? skill.description : "";
|
|
124
|
+
lines.push(" <skill>");
|
|
125
|
+
if (argKey === "id")
|
|
126
|
+
lines.push(` <id>${name}</id>`);
|
|
127
|
+
lines.push(` <name>${name}</name>`);
|
|
128
|
+
if (description)
|
|
129
|
+
lines.push(` <description>${description}</description>`);
|
|
130
|
+
lines.push(" </skill>");
|
|
131
|
+
}
|
|
132
|
+
lines.push("</available_skills>");
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* OpenCode-shaped Mid-Conversation skill update.
|
|
137
|
+
*
|
|
138
|
+
* Pure: compare `previousSkillIds` (null = not yet admitted this conversation)
|
|
139
|
+
* to the current catalog. First admission returns undefined — baseline
|
|
140
|
+
* RequestContext + routing instruction already carry the list. Later turns
|
|
141
|
+
* only speak when the id set changes (agent switch / discovery growth), matching
|
|
142
|
+
* `SkillGuidance.update` / `SkillInstructions.update`.
|
|
143
|
+
*/
|
|
144
|
+
export function buildSkillCatalogChangeReminder(options) {
|
|
145
|
+
const nextSkillIds = skillIds(options.skills);
|
|
146
|
+
if (!options.hasSkillTool) {
|
|
147
|
+
return { text: undefined, nextSkillIds: options.previousSkillIds ? [...options.previousSkillIds] : [] };
|
|
148
|
+
}
|
|
149
|
+
if (options.previousSkillIds === null) {
|
|
150
|
+
return { text: undefined, nextSkillIds };
|
|
151
|
+
}
|
|
152
|
+
if (sameIdList(options.previousSkillIds, nextSkillIds)) {
|
|
153
|
+
return { text: undefined, nextSkillIds };
|
|
154
|
+
}
|
|
155
|
+
const argKey = options.skillArgKey === "id" ? "id" : "name";
|
|
156
|
+
const callHint = argKey === "id"
|
|
157
|
+
? "Call `skill` with `{ \"id\": \"<skill-id>\" }` (host schema key is `id`)."
|
|
158
|
+
: "Call `skill` with `{ \"name\": \"<skill-id>\" }` (host schema key is `name`).";
|
|
159
|
+
const body = nextSkillIds.length === 0
|
|
160
|
+
? ["Skill guidance is no longer available. Do not use any previously listed skill."]
|
|
161
|
+
: [
|
|
162
|
+
"The available skills have changed. This list supersedes the previous available skills list.",
|
|
163
|
+
"Skills provide specialized instructions and workflows for specific tasks.",
|
|
164
|
+
"Use the skill tool to load a skill when a task matches its description.",
|
|
165
|
+
"The user may also invoke a skill directly. When that happens, its instructions appear in the conversation as a <skill_content> block, the same shape the skill tool returns. A skill that is already present this way does not need to be invoked again.",
|
|
166
|
+
`OpenCode skills are invoked with the host \`skill\` tool through GetDynamicTools / CallDynamicTool (MCP server \`${DEFAULT_TOOL_SERVER}\`, tool \`skill\`), not by Grep/Read of SKILL.md. ${callHint}`,
|
|
167
|
+
...skillEntriesXml(options.skills, argKey),
|
|
168
|
+
];
|
|
169
|
+
return {
|
|
170
|
+
text: `<system_reminder>\n${body.join("\n")}\n</system_reminder>`,
|
|
171
|
+
nextSkillIds,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Admit the current skill catalog for a conversation and return a Mid-Conversation
|
|
176
|
+
* reminder only when OpenCode would (catalog changed after baseline admission).
|
|
177
|
+
*/
|
|
178
|
+
export function takeSkillCatalogChangeReminder(conversationId, options) {
|
|
179
|
+
const previous = conversationId
|
|
180
|
+
? (admittedSkillIdsByConversation.get(conversationId) ?? null)
|
|
181
|
+
: null;
|
|
182
|
+
const { text, nextSkillIds } = buildSkillCatalogChangeReminder({
|
|
183
|
+
...options,
|
|
184
|
+
previousSkillIds: previous,
|
|
185
|
+
});
|
|
186
|
+
if (!conversationId)
|
|
187
|
+
return text;
|
|
188
|
+
if (!options.hasSkillTool && previous === null && nextSkillIds.length === 0) {
|
|
189
|
+
return text;
|
|
190
|
+
}
|
|
191
|
+
admittedSkillIdsByConversation.delete(conversationId);
|
|
192
|
+
admittedSkillIdsByConversation.set(conversationId, nextSkillIds);
|
|
193
|
+
while (admittedSkillIdsByConversation.size > 256) {
|
|
194
|
+
const oldest = admittedSkillIdsByConversation.keys().next().value;
|
|
195
|
+
if (!oldest)
|
|
196
|
+
break;
|
|
197
|
+
admittedSkillIdsByConversation.delete(oldest);
|
|
198
|
+
}
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
export function clearSkillCatalogAdmission(conversationId) {
|
|
202
|
+
admittedSkillIdsByConversation.delete(conversationId);
|
|
203
|
+
}
|
|
204
|
+
export function transferSkillCatalogAdmission(previousConversationId, nextConversationId) {
|
|
205
|
+
if (!previousConversationId || !nextConversationId)
|
|
206
|
+
return;
|
|
207
|
+
const prior = admittedSkillIdsByConversation.get(previousConversationId);
|
|
208
|
+
admittedSkillIdsByConversation.delete(previousConversationId);
|
|
209
|
+
admittedSkillIdsByConversation.delete(nextConversationId);
|
|
210
|
+
if (prior)
|
|
211
|
+
admittedSkillIdsByConversation.set(nextConversationId, prior);
|
|
212
|
+
}
|
|
213
|
+
export function resetSkillCatalogAdmissionsForTests() {
|
|
214
|
+
admittedSkillIdsByConversation.clear();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* @deprecated Prefer `buildSkillCatalogChangeReminder` / `takeSkillCatalogChangeReminder`.
|
|
218
|
+
* Kept as a thin alias for callers that still expect the old name; always returns
|
|
219
|
+
* undefined for userText-based matching (OpenCode never did per-turn id nudges).
|
|
220
|
+
*/
|
|
221
|
+
export function buildSkillCatalogNudge(options) {
|
|
222
|
+
return buildSkillCatalogChangeReminder({
|
|
223
|
+
hasSkillTool: options.hasSkillTool,
|
|
224
|
+
skills: options.skills,
|
|
225
|
+
previousSkillIds: options.previousSkillIds ?? null,
|
|
226
|
+
skillArgKey: options.skillArgKey,
|
|
227
|
+
}).text;
|
|
228
|
+
}
|
package/dist/context/frozen.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
2
|
import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js";
|
|
3
3
|
import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js";
|
|
4
|
+
import { clearSkillCatalogAdmission, resetSkillCatalogAdmissionsForTests, transferSkillCatalogAdmission, } from "./dynamic-catalog.js";
|
|
4
5
|
import { trace } from "../debug.js";
|
|
5
6
|
import { encodeMessage } from "../protocol/messages.js";
|
|
6
7
|
/**
|
|
@@ -44,6 +45,7 @@ function remember(conversationId, context) {
|
|
|
44
45
|
byConversationId.delete(oldest);
|
|
45
46
|
materializedByConversationId.delete(oldest);
|
|
46
47
|
clearOverlayHold(oldest);
|
|
48
|
+
clearSkillCatalogAdmission(oldest);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
/** Frozen stable RequestContext base for this conversation, if any. */
|
|
@@ -67,6 +69,7 @@ export function clearFrozenRequestContext(conversationId) {
|
|
|
67
69
|
byConversationId.delete(conversationId);
|
|
68
70
|
materializedByConversationId.delete(conversationId);
|
|
69
71
|
clearOverlayHold(conversationId);
|
|
72
|
+
clearSkillCatalogAdmission(conversationId);
|
|
70
73
|
clearContextEpoch(conversationId);
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
@@ -89,6 +92,7 @@ export function transferFrozenRequestContext(previousConversationId, nextConvers
|
|
|
89
92
|
// also drops epoch state for each id.
|
|
90
93
|
endContextEpoch(previousConversationId, nextConversationId);
|
|
91
94
|
transferOverlayHold(previousConversationId, nextConversationId);
|
|
95
|
+
transferSkillCatalogAdmission(previousConversationId, nextConversationId);
|
|
92
96
|
byConversationId.delete(previousConversationId);
|
|
93
97
|
materializedByConversationId.delete(previousConversationId);
|
|
94
98
|
byConversationId.delete(nextConversationId);
|
|
@@ -110,6 +114,7 @@ export function resetFrozenRequestContextsForTests() {
|
|
|
110
114
|
materializedByConversationId.clear();
|
|
111
115
|
buildsByConversationId.clear();
|
|
112
116
|
resetOverlayHoldsForTests();
|
|
117
|
+
resetSkillCatalogAdmissionsForTests();
|
|
113
118
|
resetContextEpochsForTests();
|
|
114
119
|
}
|
|
115
120
|
function sameBytes(a, b) {
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, buildSkillCatalogChangeReminder, buildSkillCatalogNudge, takeSkillCatalogChangeReminder, listAdvertisedMcpServers, skillNameFromAgentSkill, type AgentSkillLike, } from "./dynamic-catalog.js";
|
|
2
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
3
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
4
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, type AdmitContextEpochInput, type AdmitContextEpochResult, type ContextEpoch, type ContextSourceSnapshot, } from "./epoch.js";
|
package/dist/context/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, buildSkillCatalogChangeReminder, buildSkillCatalogNudge, takeSkillCatalogChangeReminder, listAdvertisedMcpServers, skillNameFromAgentSkill, } from "./dynamic-catalog.js";
|
|
2
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
3
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
4
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, } from "./epoch.js";
|
|
@@ -39,6 +39,11 @@ export type OverlayWire = {
|
|
|
39
39
|
};
|
|
40
40
|
/** Merge live discovery into the conversation overlay epoch. */
|
|
41
41
|
export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold): OverlayWire;
|
|
42
|
+
/**
|
|
43
|
+
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
44
|
+
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getHeldOverlaySkills(conversationId: string): OverlaySkill[];
|
|
42
47
|
export declare function clearOverlayHold(conversationId: string): void;
|
|
43
48
|
export declare function transferOverlayHold(previousConversationId: string, nextConversationId: string): void;
|
|
44
49
|
export declare function resetOverlayHoldsForTests(): void;
|
package/dist/context/overlay.js
CHANGED
|
@@ -78,6 +78,13 @@ export function holdCapabilityOverlay(conversationId, live) {
|
|
|
78
78
|
}
|
|
79
79
|
return toWire(remember(conversationId, next));
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
83
|
+
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
84
|
+
*/
|
|
85
|
+
export function getHeldOverlaySkills(conversationId) {
|
|
86
|
+
return structuredClone(byConversationId.get(conversationId)?.skills ?? []);
|
|
87
|
+
}
|
|
81
88
|
export function clearOverlayHold(conversationId) {
|
|
82
89
|
byConversationId.delete(conversationId);
|
|
83
90
|
}
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -17,8 +17,9 @@ export declare function fetchRemoteInstruction(url: string, timeoutMs?: number):
|
|
|
17
17
|
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
|
18
18
|
/**
|
|
19
19
|
* Collect OpenCode instruction files.
|
|
20
|
+
* `preloadedConfig` reuses a merged config already loaded on this Run.
|
|
20
21
|
*/
|
|
21
|
-
export declare function collectRules(workspaceRoot: string): Promise<{
|
|
22
|
+
export declare function collectRules(workspaceRoot: string, preloadedConfig?: OpencodeJson): Promise<{
|
|
22
23
|
rules: CollectedRule[];
|
|
23
24
|
config: OpencodeJson;
|
|
24
25
|
worktree: string;
|
package/dist/context/rules.js
CHANGED
|
@@ -178,12 +178,13 @@ export async function loadMergedConfig(workspaceRoot) {
|
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
180
180
|
* Collect OpenCode instruction files.
|
|
181
|
+
* `preloadedConfig` reuses a merged config already loaded on this Run.
|
|
181
182
|
*/
|
|
182
|
-
export async function collectRules(workspaceRoot) {
|
|
183
|
+
export async function collectRules(workspaceRoot, preloadedConfig) {
|
|
183
184
|
const worktree = await findGitWorktree(workspaceRoot);
|
|
184
185
|
const rules = [];
|
|
185
186
|
const seen = new Set();
|
|
186
|
-
const config = await loadMergedConfig(workspaceRoot);
|
|
187
|
+
const config = preloadedConfig ?? await loadMergedConfig(workspaceRoot);
|
|
187
188
|
const add = async (file) => {
|
|
188
189
|
if (!file)
|
|
189
190
|
return;
|
package/dist/image-save.d.ts
CHANGED
|
@@ -55,4 +55,4 @@ export declare function resolveContainedImagePath(target: string, allowedRoots:
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function executeCursorImageSave(args: {
|
|
57
57
|
image_id?: unknown;
|
|
58
|
-
}, ctx: ImageSaveToolContext): Promise<ImageSaveResult
|
|
58
|
+
}, ctx: ImageSaveToolContext): Promise<ImageSaveResult>;
|
package/dist/image-save.js
CHANGED
|
@@ -106,18 +106,18 @@ function isInsideProject(target, worktree, directory) {
|
|
|
106
106
|
export async function executeCursorImageSave(args, ctx) {
|
|
107
107
|
const imageId = typeof args.image_id === "string" ? args.image_id : "";
|
|
108
108
|
if (!imageId)
|
|
109
|
-
|
|
109
|
+
throw new Error("No image id was provided, so there is nothing to save.");
|
|
110
110
|
const image = takePendingCursorImage(imageId);
|
|
111
111
|
if (!image) {
|
|
112
112
|
// Expired, already committed, or never existed. Identical response for all
|
|
113
113
|
// three: nothing here should confirm whether an id was ever valid.
|
|
114
|
-
|
|
114
|
+
throw new Error("No pending Cursor image matches that id. It may have already been saved or expired.");
|
|
115
115
|
}
|
|
116
116
|
const workspace = ctx.worktree || ctx.directory;
|
|
117
117
|
const contained = resolveContainedImagePath(image.path, [image.projectDir, workspace].filter((root) => !!root));
|
|
118
118
|
if ("error" in contained) {
|
|
119
119
|
trace(`image save: refused path=${JSON.stringify(image.path)} reason=${contained.error}`);
|
|
120
|
-
|
|
120
|
+
throw new Error(`Refusing to save the generated image: ${contained.error}.`);
|
|
121
121
|
}
|
|
122
122
|
// Mirror what OpenCode's own `write` tool does, in the same order: an
|
|
123
123
|
// external-directory gate for targets outside the project, then the `edit`
|
package/dist/image-staging.js
CHANGED
|
@@ -25,7 +25,12 @@ export const MAX_STAGED_IMAGE_BYTES = 50 * 1024 * 1024;
|
|
|
25
25
|
export const STAGED_IMAGE_TTL_MS = 10 * 60_000;
|
|
26
26
|
/** Bounds memory if a turn generates several images and none are committed. */
|
|
27
27
|
const MAX_PENDING = 8;
|
|
28
|
-
|
|
28
|
+
// Some hosts load the model and the image-save subpath in separate module
|
|
29
|
+
// graphs. Keep the one-use handles in the process registry so both graphs see
|
|
30
|
+
// the same staged bytes. The UUID handle remains the only tool input.
|
|
31
|
+
const PENDING_IMAGES = Symbol.for("cursor-opencode-provider.pending-images");
|
|
32
|
+
const globals = globalThis;
|
|
33
|
+
const pending = globals[PENDING_IMAGES] ??= new Map();
|
|
29
34
|
export class StagedImageTooLargeError extends Error {
|
|
30
35
|
constructor(bytes) {
|
|
31
36
|
super(`Generated image is ${bytes} bytes, above the ${MAX_STAGED_IMAGE_BYTES} byte limit`);
|
package/dist/language-model.d.ts
CHANGED
|
@@ -68,6 +68,24 @@ export declare function findContinuationSession(toolResults: Array<{
|
|
|
68
68
|
sessionId: string;
|
|
69
69
|
execId: number;
|
|
70
70
|
}>): CursorSession | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* True when `incoming` is a non-empty proper subset of `parent` by tool name.
|
|
73
|
+
* Used to spot in-session helpers that reuse the parent OpenCode session id
|
|
74
|
+
* with a reduced catalog (e.g. stripping `task` / `question` / `plan_exit`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function isProperCatalogSubset(incoming: ReadonlyArray<{
|
|
77
|
+
name?: string;
|
|
78
|
+
}>, parent: ReadonlyArray<{
|
|
79
|
+
name?: string;
|
|
80
|
+
}>): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* An open parent Run for this OpenCode session whose catalog strictly contains
|
|
83
|
+
* the incoming tools is treated as an in-session helper. Those calls must not
|
|
84
|
+
* cancel/supersede the parent (that remints on the trailing tool result).
|
|
85
|
+
*/
|
|
86
|
+
export declare function shouldIsolateInSessionHelper(openCodeSessionId: string | undefined, incomingTools: ReadonlyArray<{
|
|
87
|
+
name?: string;
|
|
88
|
+
}>): boolean;
|
|
71
89
|
/** How long a fresh-turn drain may wait for Cursor `turn_ended` after bridged settle. */
|
|
72
90
|
export declare const FRESH_TURN_DRAIN_TIMEOUT_MS = 8000;
|
|
73
91
|
/**
|
|
@@ -133,6 +151,8 @@ type ExtractedToolResult = {
|
|
|
133
151
|
* are ignored — they are conversation history, not replies for a held-open Run.
|
|
134
152
|
*/
|
|
135
153
|
export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOptions["prompt"]): ExtractedToolResult[];
|
|
154
|
+
/** Detect a host-owned canonical plan review, excluding Cursor exec replies. */
|
|
155
|
+
export declare function hasApprovedUncorrelatedPlanStageResult(prompt: LanguageModelV3CallOptions["prompt"]): boolean;
|
|
136
156
|
/**
|
|
137
157
|
* Checkpointed Runs do not resend the system prompt. Keep the workspace root
|
|
138
158
|
* on the live user message, and require absolute `path` arguments when that is
|
|
@@ -147,7 +167,9 @@ export declare function groundCheckpointTurnText(userText: string, checkpoint: b
|
|
|
147
167
|
* Redirect only to OpenCode tools that are genuinely advertised this turn;
|
|
148
168
|
* compaction keeps its dedicated summary prompt unchanged.
|
|
149
169
|
*/
|
|
150
|
-
export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string
|
|
170
|
+
export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string, options?: {
|
|
171
|
+
knownMcpServers?: Iterable<string>;
|
|
172
|
+
}): string | undefined;
|
|
151
173
|
/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
|
|
152
174
|
export declare function estimateTokens(chars: number): number;
|
|
153
175
|
/** Preserve exact request-local Cursor counters as diagnostics. */
|