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
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
- import { trace } from "./debug.js";
11
+ import { errorMessage, trace } from "./debug.js";
12
12
  import { takePendingCursorImage } from "./image-staging.js";
13
13
  /**
14
14
  * Marks a refusal that must reach Cursor as `WriteResult.permission_denied`
@@ -126,31 +126,42 @@ export async function executeCursorImageSave(args, ctx) {
126
126
  // case `assertExternalDirectory` exists for. Asking only for `edit` would
127
127
  // quietly skip a boundary the host enforces for every one of its own writes.
128
128
  const editPattern = path.relative(realRoot(workspace), contained.path);
129
- try {
130
- if (!isInsideProject(contained.path, workspace, ctx.directory)) {
131
- const parentDir = path.dirname(contained.path);
132
- const glob = path.join(parentDir, "*").replaceAll("\\", "/");
129
+ if (typeof ctx.ask === "function") {
130
+ try {
131
+ if (!isInsideProject(contained.path, workspace, ctx.directory)) {
132
+ const parentDir = path.dirname(contained.path);
133
+ const glob = path.join(parentDir, "*").replaceAll("\\", "/");
134
+ await ctx.ask({
135
+ permission: "external_directory",
136
+ patterns: [glob],
137
+ always: [glob],
138
+ metadata: { filepath: contained.path, parentDir },
139
+ });
140
+ }
133
141
  await ctx.ask({
134
- permission: "external_directory",
135
- patterns: [glob],
136
- always: [glob],
137
- metadata: { filepath: contained.path, parentDir },
142
+ permission: "edit",
143
+ patterns: [editPattern],
144
+ always: ["*"],
145
+ metadata: {
146
+ filepath: contained.path,
147
+ mime: image.mime,
148
+ bytes: image.data.length,
149
+ source: "cursor-generate-image",
150
+ },
138
151
  });
139
152
  }
140
- await ctx.ask({
141
- permission: "edit",
142
- patterns: [editPattern],
143
- always: ["*"],
144
- metadata: {
145
- filepath: contained.path,
146
- mime: image.mime,
147
- bytes: image.data.length,
148
- source: "cursor-generate-image",
149
- },
150
- });
153
+ catch (error) {
154
+ const reason = errorMessage(error);
155
+ trace(`image save: permission refused path=${JSON.stringify(contained.path)} reason=${reason}`);
156
+ throw new Error(`${IMAGE_PERMISSION_DENIED_PREFIX} ${reason}`);
157
+ }
151
158
  }
152
- catch (error) {
153
- const reason = error instanceof Error ? error.message : String(error);
159
+ else if (ctx.ask === null) {
160
+ trace(`image save: host cannot prompt; writing after containment ` +
161
+ `path=${JSON.stringify(contained.path)}`);
162
+ }
163
+ else {
164
+ const reason = "this host did not provide a permission prompt for the write";
154
165
  trace(`image save: permission refused path=${JSON.stringify(contained.path)} reason=${reason}`);
155
166
  throw new Error(`${IMAGE_PERMISSION_DENIED_PREFIX} ${reason}`);
156
167
  }
@@ -165,7 +176,7 @@ export async function executeCursorImageSave(args, ctx) {
165
176
  catch (error) {
166
177
  // Most often something already occupies a path component (EEXIST/ENOTDIR).
167
178
  // Say which path is blocked; the raw errno alone tells the model nothing.
168
- const reason = error instanceof Error ? error.message : String(error);
179
+ const reason = errorMessage(error);
169
180
  trace(`image save: write failed path=${JSON.stringify(contained.path)} reason=${reason}`);
170
181
  throw new Error(`Could not save the generated image to ${contained.path}: ${reason}. `
171
182
  + `Check that ${directory} is a writable directory.`);
@@ -185,5 +196,6 @@ export async function executeCursorImageSave(args, ctx) {
185
196
  return {
186
197
  title: editPattern,
187
198
  output: `Saved the generated image to ${contained.path} (${image.data.length} bytes).`,
199
+ bytes: image.data.length,
188
200
  };
189
201
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CursorPlugin } from "./plugin.js";
2
2
  import type { CursorContinuationOptions } from "./session.js";
3
3
  import type { HostToolDialect } from "./protocol/tools.js";
4
+ import type { AccessTokenProvider } from "./auth-renewal.js";
4
5
  export type CursorRetryOptions = {
5
6
  /** Total attempts including the initial request. Default: 3. */
6
7
  maxAttempts?: number;
@@ -11,7 +12,16 @@ export type CursorRetryOptions = {
11
12
  };
12
13
  export type CreateCursorOptions = {
13
14
  name: string;
15
+ /**
16
+ * Current Cursor access token, asked for every time a Run opens. Takes
17
+ * precedence over `accessToken` and `apiKey`; hosts use it to renew the
18
+ * login without exposing a secret in serializable options. `forceRefresh`
19
+ * is set once after Cursor rejected the previous token.
20
+ */
21
+ getAccessToken?: AccessTokenProvider;
22
+ /** Access token sent as-is (no renewal). Used when `getAccessToken` is absent. */
14
23
  accessToken?: string;
24
+ /** Raw `crsr_` API key, exchanged and renewed. Used only when neither of the above is given. */
15
25
  apiKey?: string;
16
26
  /** API base for auth, model discovery, and GetServerConfig. */
17
27
  apiBaseURL?: string;
@@ -5,7 +5,9 @@ import { cursorContextUsageMetadata, type CursorContextUsageSource, type CursorC
5
5
  import { type ConversationBlobGraphStats } from "./protocol/blob-store.js";
6
6
  import { type CursorSession } from "./session.js";
7
7
  import { CursorProviderError } from "./errors.js";
8
+ import { type ModelInfo } from "./models.js";
8
9
  import type { SeedHistoryMessage } from "./protocol/request.js";
10
+ import { type CursorImageInput } from "./image-input.js";
9
11
  type PromptIdentity = {
10
12
  hostAgent?: string;
11
13
  systemPromptHash?: string;
@@ -21,6 +23,13 @@ export declare const MAX_CHECKPOINT_BLOB_GRAPH_BYTES: number;
21
23
  export declare function checkpointBlobGraphRequiresRebase(_stats: ConversationBlobGraphStats, _maxBytes?: number): boolean;
22
24
  /** Warn-only concern for incomplete / oversized checkpoint graphs (no remint). */
23
25
  export declare function checkpointBlobGraphConcern(stats: ConversationBlobGraphStats, maxBytes?: number): "incomplete-checkpoint-graph" | "oversized-checkpoint-graph" | undefined;
26
+ /**
27
+ * True when the raw frame holds exactly one control message: a KV request, a
28
+ * checkpoint update, or an interaction update that is only a heartbeat. Extra
29
+ * fields inside a KV request are tolerated (Cursor has sent them live); extra
30
+ * top-level fields are not.
31
+ */
32
+ export declare function isSoleControlFrame(payload: Uint8Array): boolean;
24
33
  export type CursorRetryPolicy = {
25
34
  maxAttempts: number;
26
35
  baseDelayMs: number;
@@ -28,7 +37,7 @@ export type CursorRetryPolicy = {
28
37
  };
29
38
  export declare function resolveRetryPolicy(options: CursorRetryOptions | undefined): CursorRetryPolicy;
30
39
  export declare function connectFrameError(payload: string): CursorProviderError;
31
- /** Restore the last real catalog solely for lifecycle turns such as compaction. */
40
+ /** Restore the epoch catalog for the next nonempty host turn. */
32
41
  export declare function restoreTurnToolCatalog(sessionKey: string, tools: OpencodeToolDef[]): void;
33
42
  /** Record the authoritative host todo list for an OpenCode session. */
34
43
  export declare function rememberMirroredTodos(sessionKey: string | undefined, todos: ReadonlyArray<Record<string, unknown>>): void;
@@ -40,6 +49,8 @@ export declare function snapshotMirroredTodosBySession(sessionKey: string | unde
40
49
  * churn have a single no-op definition.
41
50
  */
42
51
  export declare function promptIdentityWouldRemint(_previous: PromptIdentity, _current: PromptIdentity): boolean;
52
+ /** Test helper: content hashes recorded after a successful held-Run image write. */
53
+ export declare function snapshotSentHistoryImageHashesForTests(sessionKey: string): string[];
43
54
  type V3Part = LanguageModelV3StreamPart;
44
55
  export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3;
45
56
  export declare function pumpWithRecovery(input: {
@@ -50,9 +61,17 @@ export declare function pumpWithRecovery(input: {
50
61
  recover: (recovery: CursorRunRecovery) => Promise<CursorSession>;
51
62
  onSession?: (session: CursorSession) => void;
52
63
  maxRecoveries?: number;
64
+ /**
65
+ * Set when the credential source can renew: called once per turn after
66
+ * Cursor rejects the token (401 / unauthenticated) before the attempt did
67
+ * anything, so the reopened Run uses a force-renewed token. This retry does
68
+ * not count against the transient-failure budget.
69
+ */
70
+ renewRejectedCredential?: () => void;
53
71
  }): Promise<CursorSession>;
54
72
  export type CursorRunRecovery = {
55
73
  kind: "rebase";
74
+ reason?: "checkpoint-unusable";
56
75
  } | {
57
76
  kind: "resume";
58
77
  conversationId: string;
@@ -80,12 +99,14 @@ export declare function isProperCatalogSubset(incoming: ReadonlyArray<{
80
99
  }>): boolean;
81
100
  /**
82
101
  * 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).
102
+ * the incoming tools is treated as an in-session helper unless the request
103
+ * carries a result for that parent's pending call. A result can precede a new
104
+ * user message or agent-change reminder and still needs to reach the parent.
105
+ * Helper calls must not cancel/supersede the parent.
85
106
  */
86
107
  export declare function shouldIsolateInSessionHelper(openCodeSessionId: string | undefined, incomingTools: ReadonlyArray<{
87
108
  name?: string;
88
- }>): boolean;
109
+ }>, historicalResults?: ExtractedToolResult[]): boolean;
89
110
  /** How long a fresh-turn drain may wait for Cursor `turn_ended` after bridged settle. */
90
111
  export declare const FRESH_TURN_DRAIN_TIMEOUT_MS = 8000;
91
112
  /**
@@ -103,6 +124,9 @@ export declare const FRESH_TURN_PENDING_CANCEL_REASON = "Host started a new user
103
124
  */
104
125
  export declare function preparePriorSessionForFreshTurn(openCodeSessionId: string | undefined, opts?: {
105
126
  timeoutMs?: number;
127
+ /** Tool results already in this prompt; deliver any that match held pendings before cancel. */
128
+ toolResults?: ExtractedToolResult[];
129
+ hostAgent?: string;
106
130
  }): Promise<"drained" | "settled-only" | "busy" | "none">;
107
131
  /**
108
132
  * Write error/reject results for every still-open non-bridged pending on the
@@ -119,44 +143,51 @@ export declare function cancelPendingExecsForFreshTurn(session: CursorSession):
119
143
  export declare function drainSessionUntilTurnEnded(session: CursorSession, opts?: {
120
144
  timeoutMs?: number;
121
145
  }): Promise<"turn-ended" | "busy" | "timeout" | "interrupted" | "skipped">;
122
- /**
123
- * Deliver trailing tool results onto a live continuation session.
124
- * Returns the same session when writes succeed (or only bridged results were
125
- * cleared). Returns undefined after closing the session when a write fails, so
126
- * the caller can rebase onto a fresh Run instead of pumping a dead stream.
127
- */
128
- export declare function deliverContinuationResults(session: CursorSession, trailingToolResults: ExtractedToolResult[]): CursorSession | undefined;
146
+ export declare function deliverContinuationResults(session: CursorSession, trailingToolResults: ExtractedToolResult[],
147
+ /** Host primary agent of the request that carries these results. */
148
+ delivery?: {
149
+ hostAgent?: string;
150
+ }): CursorSession | undefined;
129
151
  /**
130
152
  * Read the held-open stream, emitting stream parts, until the turn boundary:
131
- * - a tool call (exec_server_message) → emit tool-call, finish "tool-calls",
132
- * and KEEP the session open for the result on the next doStream call;
153
+ * - tool call(s) for one Cursor generation → emit each tool-call as it
154
+ * arrives; finish "tool-calls" once Cursor's listed count (field 27) is
155
+ * met (or immediately when the process has never seen field 27); KEEP the
156
+ * session open for results on the next doStream call;
133
157
  * - turn_ended → finish "stop" and close the session;
134
- * - transport EOF before turn_ended → throw for one fresh-Run recovery.
158
+ * - transport EOF before turn_ended → throw for one fresh-Run recovery
159
+ * (after host tools were already emitted, finish "tool-calls" instead and
160
+ * let the continuation rebase).
135
161
  */
136
162
  export declare function pump(session: CursorSession, controller: ReadableStreamDefaultController<V3Part>, ids: {
137
163
  textId: string;
138
164
  reasoningId: string;
139
165
  }, abortSignal?: AbortSignal): Promise<void>;
140
- type ExtractedToolResult = {
166
+ type ToolResultContent = {
141
167
  toolCallId: string;
142
- sessionId: string;
143
- execId: number;
144
168
  toolName: string;
145
169
  output: string;
146
170
  error?: string;
171
+ /** Host media parts of this result (own content, or the trailing media message). */
172
+ media?: unknown[];
173
+ /** `media` decoded for the exec result, with content hashes in the same order. */
174
+ images?: CursorImageInput[];
175
+ imageHashes?: string[];
176
+ /** Host updates that a binary read cannot carry in its output oneof. */
177
+ notes?: string;
178
+ };
179
+ type ExtractedToolResult = ToolResultContent & {
180
+ sessionId: string;
181
+ execId: number;
147
182
  };
148
- /**
149
- * Tool results that form a live continuation: only the trailing run of `tool`
150
- * messages after the last non-tool message. Mid-prompt historical tool results
151
- * are ignored — they are conversation history, not replies for a held-open Run.
152
- */
153
183
  export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOptions["prompt"]): ExtractedToolResult[];
184
+ /** Decode only image-bearing pending execs, under one continuation-wide budget. */
185
+ export declare function decodeTrailingToolImages(session: CursorSession, results: ExtractedToolResult[], signal?: AbortSignal, maxBytes?: number): Promise<ExtractedToolResult[]>;
154
186
  /** Detect a host-owned canonical plan review, excluding Cursor exec replies. */
155
187
  export declare function hasApprovedUncorrelatedPlanStageResult(prompt: LanguageModelV3CallOptions["prompt"]): boolean;
156
188
  /**
157
- * Checkpointed Runs do not resend the system prompt. Keep the workspace root
158
- * on the live user message, and require absolute `path` arguments when that is
159
- * the host's file-tool dialect.
189
+ * Keep the workspace root on a checkpointed turn's live user message too, and
190
+ * require absolute `path` arguments when that is the host's file-tool dialect.
160
191
  */
161
192
  export declare function groundCheckpointTurnText(userText: string, checkpoint: boolean, workspaceRoot: string, tools: readonly {
162
193
  name?: string;
@@ -169,6 +200,7 @@ export declare function groundCheckpointTurnText(userText: string, checkpoint: b
169
200
  */
170
201
  export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string, options?: {
171
202
  knownMcpServers?: Iterable<string>;
203
+ allowTools?: boolean;
172
204
  }): string | undefined;
173
205
  /** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
174
206
  export declare function estimateTokens(chars: number): number;
@@ -196,6 +228,21 @@ export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions[
196
228
  preserveTrailingUser?: boolean;
197
229
  toolResults?: "omit" | "all" | "trailing";
198
230
  }): SeedHistoryMessage[];
231
+ /** Share of the target context a foreign-history rebase may fill before compaction. */
232
+ export declare const FOREIGN_HISTORY_REBASE_CONTEXT_SHARE = 0.8;
233
+ /**
234
+ * A foreign-history rebase replays the full host history. When that cannot fit,
235
+ * fail before opening a Run with an error hosts classify as context overflow
236
+ * (HTTP 413 + "prompt is too long"), so the host compacts and retries.
237
+ */
238
+ export declare function assertForeignHistoryRebaseFits(input: {
239
+ modelInfo: ModelInfo | undefined;
240
+ cursorModelId: string;
241
+ maxMode: boolean;
242
+ history: SeedHistoryMessage[];
243
+ systemPrompt: string | undefined;
244
+ userText: string;
245
+ }): void;
199
246
  /** OpenCode session id header, if present. */
200
247
  export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined;
201
248
  /**
@@ -205,6 +252,14 @@ export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptio
205
252
  */
206
253
  export declare function resolveConversationId(callOptions: LanguageModelV3CallOptions): string;
207
254
  export { sessionIdToUuid } from "./protocol/conversation-bind.js";
255
+ /**
256
+ * Grow the held Run's advertised + permitted catalog from this `doStream`
257
+ * call. Continuation skips `startSession`, so without this, exec #36 and
258
+ * permission keep the freeze from Run open. MCP server ids stay those of the
259
+ * Run's one merged-config load (`startSession`); a tool that connects later
260
+ * still gets its identity from that set.
261
+ */
262
+ export declare function refreshHeldSessionToolCatalog(session: CursorSession, callOptions: LanguageModelV3CallOptions): Promise<void>;
208
263
  /** Exported for tests — AI SDK V3 span ends that must precede finish / tool-call. */
209
264
  export declare function spanEndParts(opts: {
210
265
  textStarted: boolean;
@@ -217,6 +272,12 @@ export declare function spanEndParts(opts: {
217
272
  }>;
218
273
  /** Exported for tests — false for compaction/summary (no tools) and toolChoice none. */
219
274
  export declare function computeAllowTools(toolCount: number, toolChoice: LanguageModelV3CallOptions["toolChoice"] | undefined): boolean;
275
+ /**
276
+ * OpenCode title/summary take the first non-empty line of the answer. After a
277
+ * lifecycle refuse, Cursor often narrates that tools are unavailable — drop
278
+ * that so it does not become the session title.
279
+ */
280
+ export declare function shouldDropLifecycleToollessText(text: string, refusedExec: boolean): boolean;
220
281
  export declare function resolveTurnToolState(input: {
221
282
  sessionKey?: string;
222
283
  incomingTools: OpencodeToolDef[];