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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -1,5 +1,6 @@
1
1
  import { type OpencodeToolDef } from "../protocol/tools.js";
2
2
  import { type OpencodeJson } from "./rules.js";
3
+ import { hostSkillFiles, loadBridgeSkills } from "./skills.js";
3
4
  export type BuildRequestContextInput = {
4
5
  workspaceRoot: string;
5
6
  tools?: OpencodeToolDef[];
@@ -11,20 +12,74 @@ export type BuildRequestContextInput = {
11
12
  * guidance MCP server ids). Skips a second `loadMergedConfig` disk read.
12
13
  */
13
14
  mergedConfig?: OpencodeJson;
15
+ /** Host system context to deliver as the frozen system-instructions rule. */
16
+ systemInstructions?: SystemInstructions;
17
+ /** Optional host session id for `opencode.host.skills` list(). */
18
+ sessionID?: string;
14
19
  };
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"];
20
+ /**
21
+ * Host system context (OpenCode's system prompt plus this provider's
22
+ * interaction guidance) for one Cursor conversation.
23
+ */
24
+ export type SystemInstructions = {
25
+ text: string;
26
+ /**
27
+ * True when `text` is this conversation's own frozen baseline (a Context
28
+ * Epoch started in this process, or an ephemeral Run's prompt): it replaces
29
+ * any rule already frozen. False when a restart recovered the epoch without
30
+ * its bytes: the persisted rule wins and `text` only fills a missing one.
31
+ */
32
+ authoritative: boolean;
33
+ };
34
+ /** Rule name Cursor shows for the host system context (not a file). */
35
+ export declare const SYSTEM_INSTRUCTIONS_RULE_PATH = "OpenCode system instructions";
36
+ /**
37
+ * Cursor does not follow a client-seeded `system` message, and applies a rule
38
+ * only by its type. The Cursor CLI sends AGENTS.md as `alwaysApply`
39
+ * (`CursorRuleType.global`) and the IDE ships its own guidance as a non-file
40
+ * global rule; the host system context goes out the same way, once, frozen
41
+ * with the RequestContext base.
42
+ */
43
+ export declare function systemInstructionsRule(text: string): Record<string, unknown>;
44
+ /** Text of the frozen system-instructions rule, if the context carries one. */
45
+ export declare function systemInstructionsRuleText(context: Record<string, unknown>): string | undefined;
46
+ /**
47
+ * Return `context` with the system-instructions rule applied, or the same
48
+ * object when nothing changes (see `SystemInstructions.authoritative`).
49
+ */
50
+ export declare function withSystemInstructions(context: Record<string, unknown>, instructions: SystemInstructions | undefined): Record<string, unknown>;
51
+ export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
16
52
  export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
53
+ /**
54
+ * Derived from the system-instructions rule on every materialization (see
55
+ * `skills.ts`); never kept on a frozen base.
56
+ */
57
+ export declare const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS: readonly ["agent_skills", "agent_skills_info_complete"];
17
58
  /**
18
59
  * Full RequestContext payload for live UMA + exec #10 reply.
19
- * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks).
20
- * Honors `instructions` globs the same way OpenCode does. The provider never
21
- * looks in Cursor's own directories; a `.cursor/` path is read only when the
22
- * user's own OpenCode config lists it, exactly as the host would read it.
60
+ * Workspace env/git/layout, the host system-instructions rule, and
61
+ * host-advertised tools and subagents. The provider never looks in Cursor's
62
+ * own directories.
23
63
  */
24
64
  export declare function buildRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
65
+ /** Locations for path-desc materialization (bridge, then remembered OC2 files). */
66
+ export declare function resolveSkillLocations(input: Pick<BuildRequestContextInput, "tools" | "sessionID">, workspaceRoot: string): Promise<{
67
+ bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
68
+ skillFiles?: ReturnType<typeof hostSkillFiles>;
69
+ }>;
25
70
  /** Rediscover only capability/plugin sections that may change during a chat. */
26
71
  export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
27
- /** Keep expensive workspace state frozen while replacing every live capability field. */
28
- export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>): Record<string, unknown>;
72
+ export type MaterializeRequestContextOptions = {
73
+ tools?: OpencodeToolDef[];
74
+ skillFiles?: ReadonlyMap<string, string>;
75
+ bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
76
+ };
77
+ /**
78
+ * Keep expensive workspace state frozen while replacing every live capability
79
+ * field. Skill locations (bridge / OpenCode 2 files / OpenCode 1 catalog
80
+ * paths) turn the skill catalog in the system-instructions rule into Cursor's
81
+ * path-desc `agent_skills`.
82
+ */
83
+ export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>, options?: MaterializeRequestContextOptions): Record<string, unknown>;
29
84
  /** Strip live capability fields before retaining/persisting a conversation base. */
30
85
  export declare function requestContextBase(context: Record<string, unknown>): Record<string, unknown>;
@@ -1,84 +1,116 @@
1
1
  import path from "node:path";
2
- import { extractHostSubagentCatalog, toolsToDescriptors, toolsToMcpDescriptors, } from "../protocol/tools.js";
3
- import { collectRules, findGitWorktree, loadMergedConfig, } from "./rules.js";
4
- import { collectSkills } from "./skills.js";
5
- import { collectAgents } from "./agents.js";
2
+ import { extractHostSubagentCatalog, toolsToMcpDescriptors, } from "../protocol/tools.js";
3
+ import { loadMergedConfig, } from "./rules.js";
6
4
  import { collectPlugins } from "./plugins.js";
7
5
  import { collectGit } from "./git.js";
8
6
  import { collectProjectLayout } from "./layout.js";
9
7
  import { buildEnv } from "./env.js";
10
8
  import { ensureOpencodeProjectDir } from "./paths.js";
11
9
  import { holdCapabilityOverlay } from "./overlay.js";
10
+ import { agentSkillsForCursor, applyAgentSkillsToContext, hostSkillFiles, loadBridgeSkills, skillToolAdvertised, } from "./skills.js";
12
11
  import { traceRequestContextPaths } from "../debug.js";
12
+ /** Rule name Cursor shows for the host system context (not a file). */
13
+ export const SYSTEM_INSTRUCTIONS_RULE_PATH = "OpenCode system instructions";
14
+ /**
15
+ * Cursor does not follow a client-seeded `system` message, and applies a rule
16
+ * only by its type. The Cursor CLI sends AGENTS.md as `alwaysApply`
17
+ * (`CursorRuleType.global`) and the IDE ships its own guidance as a non-file
18
+ * global rule; the host system context goes out the same way, once, frozen
19
+ * with the RequestContext base.
20
+ */
21
+ export function systemInstructionsRule(text) {
22
+ return {
23
+ full_path: SYSTEM_INSTRUCTIONS_RULE_PATH,
24
+ content: text,
25
+ type: { global: {} },
26
+ };
27
+ }
28
+ function isSystemInstructionsRule(rule) {
29
+ if (!rule || typeof rule !== "object")
30
+ return false;
31
+ const record = rule;
32
+ const type = record.type;
33
+ return record.full_path === SYSTEM_INSTRUCTIONS_RULE_PATH && !!type?.global
34
+ && typeof record.content === "string";
35
+ }
36
+ /** Text of the frozen system-instructions rule, if the context carries one. */
37
+ export function systemInstructionsRuleText(context) {
38
+ const rules = Array.isArray(context.rules) ? context.rules : [];
39
+ const rule = rules.find(isSystemInstructionsRule);
40
+ return rule ? rule.content : undefined;
41
+ }
42
+ /**
43
+ * Return `context` with the system-instructions rule applied, or the same
44
+ * object when nothing changes (see `SystemInstructions.authoritative`).
45
+ */
46
+ export function withSystemInstructions(context, instructions) {
47
+ const text = instructions?.text.trim() ? instructions.text : undefined;
48
+ if (!text)
49
+ return context;
50
+ const current = systemInstructionsRuleText(context);
51
+ if (current !== undefined && (current === text || !instructions.authoritative))
52
+ return context;
53
+ return { ...context, rules: [systemInstructionsRule(text)] };
54
+ }
55
+ /** Keep only the system-instructions rule; older bases carried untyped rules Cursor never applied. */
56
+ function keepSystemInstructionsRule(context) {
57
+ if (!Object.hasOwn(context, "rules"))
58
+ return;
59
+ const rules = Array.isArray(context.rules) ? context.rules.filter(isSystemInstructionsRule) : [];
60
+ if (rules.length > 0)
61
+ context.rules = rules.slice(0, 1);
62
+ else
63
+ delete context.rules;
64
+ }
13
65
  export const DYNAMIC_REQUEST_CONTEXT_KEYS = [
14
66
  "tools",
15
- "agent_skills",
16
67
  "custom_subagents",
17
68
  "mcp_file_system_options",
18
69
  "mcp_meta_tool_options",
19
70
  "web_search_enabled",
20
71
  "web_fetch_enabled",
21
- "agent_skills_info_complete",
22
72
  "custom_subagents_info_complete",
23
73
  "mcp_file_system_info_complete",
24
74
  "mcp_info_complete",
25
75
  "hooks_additional_context",
26
76
  ];
27
- const DEFAULT_HOST_SUBAGENTS = [
28
- {
29
- name: "general",
30
- description: "General-purpose agent for complex research and multi-step tasks.",
31
- },
32
- {
33
- name: "explore",
34
- description: "Read-only agent for searching and understanding the local codebase.",
35
- },
77
+ /**
78
+ * Derived from the system-instructions rule on every materialization (see
79
+ * `skills.ts`); never kept on a frozen base.
80
+ */
81
+ export const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS = [
82
+ "agent_skills",
83
+ "agent_skills_info_complete",
36
84
  ];
37
85
  /**
38
- * Convert the raw host executor catalog into the exact subagent list advertised
39
- * to Cursor. `hostSubagents.complete` answers a narrow question: did the host's
40
- * task tool itself exposes an exhaustive recipient list (schema enum or
41
- * catalog marker)? OpenCode's task tool intentionally uses a plain string, so
42
- * that raw flag is false even though we can still advertise a complete Cursor
43
- * catalog by adding built-in defaults and discovered agent files.
44
- *
45
- * `custom_subagents_info_complete` must describe the final advertised list, not
46
- * the raw extraction source. If there is no executor, the empty list is
47
- * complete. If the host catalog is complete, use it verbatim. Otherwise we make
48
- * the advertised set complete by augmenting with default host agents and all
49
- * locally discovered agents.
86
+ * Advertise only what the host `task` / `subagent` catalog already named.
50
87
  */
51
- function buildAdvertisedSubagentCatalog(hostSubagents, discoveredAgents) {
88
+ function buildAdvertisedSubagentCatalog(hostSubagents) {
52
89
  if (!hostSubagents.executor)
53
90
  return { agents: [], complete: true };
54
- if (hostSubagents.complete)
55
- return { agents: hostSubagents.agents, complete: true };
56
- return {
57
- agents: [...DEFAULT_HOST_SUBAGENTS, ...hostSubagents.agents, ...discoveredAgents],
58
- complete: true,
59
- };
91
+ return { agents: hostSubagents.agents, complete: hostSubagents.complete };
92
+ }
93
+ function stripHostDuplicatedRequestContextFields(context) {
94
+ for (const key of HOST_DUPLICATED_REQUEST_CONTEXT_KEYS)
95
+ delete context[key];
96
+ keepSystemInstructionsRule(context);
60
97
  }
61
98
  /**
62
99
  * Full RequestContext payload for live UMA + exec #10 reply.
63
- * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks).
64
- * Honors `instructions` globs the same way OpenCode does. The provider never
65
- * looks in Cursor's own directories; a `.cursor/` path is read only when the
66
- * user's own OpenCode config lists it, exactly as the host would read it.
100
+ * Workspace env/git/layout, the host system-instructions rule, and
101
+ * host-advertised tools and subagents. The provider never looks in Cursor's
102
+ * own directories.
67
103
  */
68
104
  export async function buildRequestContext(input) {
69
105
  const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
70
- const { rules, config, worktree } = await collectRules(workspaceRoot, input.mergedConfig);
106
+ const config = input.mergedConfig ?? await loadMergedConfig(workspaceRoot);
71
107
  const [dynamic, git, layout] = await Promise.all([
72
- buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config),
108
+ buildDynamicRequestContextFromDiscovery(input, workspaceRoot, config),
73
109
  collectGit(workspaceRoot),
74
110
  collectProjectLayout(workspaceRoot),
75
111
  ]);
76
- const base = {
112
+ const workspace = {
77
113
  env: buildEnv(workspaceRoot),
78
- rules: rules.map((r) => ({
79
- full_path: r.fullPath,
80
- content: r.content,
81
- })),
82
114
  repository_info: git.repositoryInfo,
83
115
  git_repos: git.gitRepos,
84
116
  project_layouts: [layout],
@@ -88,47 +120,41 @@ export async function buildRequestContext(input) {
88
120
  git_repo_info_complete: true,
89
121
  git_status_info_complete: true,
90
122
  };
91
- const ctx = materializeRequestContext(base, dynamic);
123
+ const base = withSystemInstructions(workspace, input.systemInstructions);
124
+ const skillLocations = await resolveSkillLocations(input, workspaceRoot);
125
+ const ctx = materializeRequestContext(base, dynamic, {
126
+ tools: input.tools,
127
+ ...skillLocations,
128
+ });
92
129
  traceRequestContextPaths("buildRequestContext", ctx);
93
130
  return ctx;
94
131
  }
95
- async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config) {
132
+ /** Locations for path-desc materialization (bridge, then remembered OC2 files). */
133
+ export async function resolveSkillLocations(input, workspaceRoot) {
134
+ if (!skillToolAdvertised(input.tools))
135
+ return {};
136
+ return {
137
+ bridgeSkills: await loadBridgeSkills({
138
+ workspaceRoot,
139
+ sessionID: input.sessionID,
140
+ }),
141
+ skillFiles: hostSkillFiles(workspaceRoot),
142
+ };
143
+ }
144
+ async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, config) {
96
145
  const providerIdentifier = input.providerIdentifier ?? "opencode";
97
146
  const tools = input.tools ?? [];
98
- const [skills, agents, plugins] = await Promise.all([
99
- collectSkills(workspaceRoot, worktree),
100
- collectAgents(workspaceRoot),
101
- collectPlugins(workspaceRoot, config),
102
- ]);
147
+ const plugins = await collectPlugins(workspaceRoot, config);
103
148
  const mcpServerNames = Object.keys(config.mcp ?? {});
104
- const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames);
105
- const nested = toolsToMcpDescriptors(tools, providerIdentifier, mcpServerNames);
149
+ const slim = toolsToMcpDescriptors(tools, providerIdentifier, mcpServerNames, { namesOnly: true });
106
150
  const projectDir = ensureOpencodeProjectDir(workspaceRoot);
107
151
  const hostSubagents = extractHostSubagentCatalog(tools);
108
- const advertisedSubagents = buildAdvertisedSubagentCatalog(hostSubagents, agents);
109
- const discoveredByName = new Map(agents.map((agent) => [agent.name, agent]));
110
- const advertisedByName = new Map();
111
- for (const agent of advertisedSubagents.agents) {
112
- if (!advertisedByName.has(agent.name))
113
- advertisedByName.set(agent.name, agent);
114
- }
115
- const customSubagents = [...advertisedByName.values()].map((agent) => {
116
- const discovered = discoveredByName.get(agent.name);
117
- return {
118
- full_path: discovered?.fullPath ?? "",
119
- name: agent.name,
120
- description: discovered?.description || agent.description || "Host-configured subagent.",
121
- // The host applies the real configured prompt when Task/Actor executes.
122
- // Cursor only needs enough context to select the recipient intentionally.
123
- prompt: discovered?.prompt ||
124
- `Delegate to the host-configured ${agent.name} subagent; its host instructions and tools apply.`,
125
- };
126
- });
127
- const liveSkills = skills.map((s) => ({
128
- id: s.name,
129
- full_path: s.fullPath,
130
- content: s.content,
131
- description: s.description,
152
+ const advertisedSubagents = buildAdvertisedSubagentCatalog(hostSubagents);
153
+ const customSubagents = advertisedSubagents.agents.map((agent) => ({
154
+ full_path: "",
155
+ name: agent.name,
156
+ description: agent.description || "Host-configured subagent.",
157
+ prompt: `Delegate to the host-configured ${agent.name} subagent; its host instructions and tools apply.`,
132
158
  }));
133
159
  const livePlugins = plugins.map((p) => ({
134
160
  id: p.id,
@@ -136,24 +162,19 @@ async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, wor
136
162
  }));
137
163
  const overlay = input.conversationId
138
164
  ? holdCapabilityOverlay(input.conversationId, {
139
- skills: liveSkills,
140
165
  subagents: customSubagents,
141
166
  plugins: livePlugins,
142
167
  })
143
- : { skills: liveSkills.map(({ full_path, content, description }) => ({ full_path, content, description })), subagents: customSubagents, plugins: livePlugins };
168
+ : { subagents: customSubagents, plugins: livePlugins };
144
169
  const dynamic = {
145
- tools: flat,
146
- agent_skills: overlay.skills,
147
- custom_subagents: overlay.subagents,
148
170
  mcp_file_system_options: {
149
171
  enabled: true,
150
172
  // Cursor metadata root (mcps / agent-tools), not the git workspace.
151
173
  workspace_project_dir: projectDir,
152
- mcp_descriptors: nested,
153
174
  },
154
175
  mcp_meta_tool_options: {
155
176
  enabled: true,
156
- mcp_descriptors: nested,
177
+ ...(slim.length > 0 ? { mcp_descriptors: slim } : {}),
157
178
  },
158
179
  // This provider always rejects native web_search/web_fetch interaction
159
180
  // queries with a headless-UI reason (see interactions.ts). Advertise that
@@ -161,11 +182,12 @@ async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, wor
161
182
  // custom_web* aliases instead of routing through a query doomed to fail.
162
183
  web_search_enabled: false,
163
184
  web_fetch_enabled: false,
164
- agent_skills_info_complete: true,
165
185
  custom_subagents_info_complete: advertisedSubagents.complete,
166
186
  mcp_file_system_info_complete: true,
167
187
  mcp_info_complete: true,
168
188
  };
189
+ if (overlay.subagents.length > 0)
190
+ dynamic.custom_subagents = overlay.subagents;
169
191
  if (overlay.plugins.length > 0) {
170
192
  dynamic.hooks_additional_context = overlay.plugins.map((p) => p.line).join("\n");
171
193
  }
@@ -174,28 +196,38 @@ async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, wor
174
196
  /** Rediscover only capability/plugin sections that may change during a chat. */
175
197
  export async function buildDynamicRequestContext(input) {
176
198
  const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
177
- const [worktree, config] = await Promise.all([
178
- findGitWorktree(workspaceRoot),
179
- input.mergedConfig
180
- ? Promise.resolve(input.mergedConfig)
181
- : loadMergedConfig(workspaceRoot),
182
- ]);
183
- return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config);
199
+ const config = input.mergedConfig
200
+ ? input.mergedConfig
201
+ : await loadMergedConfig(workspaceRoot);
202
+ return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, config);
184
203
  }
185
- /** Keep expensive workspace state frozen while replacing every live capability field. */
186
- export function materializeRequestContext(base, dynamic) {
204
+ /**
205
+ * Keep expensive workspace state frozen while replacing every live capability
206
+ * field. Skill locations (bridge / OpenCode 2 files / OpenCode 1 catalog
207
+ * paths) turn the skill catalog in the system-instructions rule into Cursor's
208
+ * path-desc `agent_skills`.
209
+ */
210
+ export function materializeRequestContext(base, dynamic, options) {
187
211
  const context = structuredClone(base);
212
+ stripHostDuplicatedRequestContextFields(context);
188
213
  for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
189
214
  delete context[key];
190
215
  for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) {
191
216
  if (Object.hasOwn(dynamic, key))
192
217
  context[key] = structuredClone(dynamic[key]);
193
218
  }
219
+ const skills = agentSkillsForCursor(systemInstructionsRuleText(context), {
220
+ skillToolAdvertised: skillToolAdvertised(options?.tools),
221
+ skillFiles: options?.skillFiles,
222
+ bridgeSkills: options?.bridgeSkills,
223
+ });
224
+ applyAgentSkillsToContext(context, skills);
194
225
  return context;
195
226
  }
196
227
  /** Strip live capability fields before retaining/persisting a conversation base. */
197
228
  export function requestContextBase(context) {
198
229
  const base = structuredClone(context);
230
+ stripHostDuplicatedRequestContextFields(base);
199
231
  for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
200
232
  delete base[key];
201
233
  return base;
@@ -1,10 +1,3 @@
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
1
  /**
9
2
  * Configured MCP servers that own at least one advertised tool. Uses the same
10
3
  * server resolution as the RequestContext descriptors, so guidance never names
@@ -12,63 +5,14 @@ export type AgentSkillLike = {
12
5
  * `sourceName` when present).
13
6
  */
14
7
  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
8
  /**
18
9
  * Shared system-guidance line: name advertised dynamic-catalog tools and prefer
19
10
  * 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
11
+ * stay out of the frozen baseline (they live in the host system prompt / skill
12
+ * tool, and in host `<system-update>` when the catalog changes — same split as
22
13
  * OpenCode's SkillGuidance / SkillInstructions).
23
14
  */
24
15
  export declare function buildDynamicCatalogRoutingInstruction(options: {
25
16
  toolNames: Iterable<string>;
26
17
  knownMcpServers?: Iterable<string>;
27
18
  }): 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;