cursor-opencode-provider 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +50 -180
  2. package/dist/compaction-marker.d.ts +3 -2
  3. package/dist/compaction-marker.js +3 -2
  4. package/dist/host-agent-mode.d.ts +32 -0
  5. package/dist/host-agent-mode.js +92 -0
  6. package/dist/language-model.d.ts +6 -1
  7. package/dist/language-model.js +170 -34
  8. package/dist/models.d.ts +12 -1
  9. package/dist/models.js +16 -6
  10. package/dist/opencode2/catalog.d.ts +46 -11
  11. package/dist/opencode2/catalog.js +60 -45
  12. package/dist/opencode2/todo-tools.d.ts +64 -0
  13. package/dist/opencode2/todo-tools.js +165 -0
  14. package/dist/opencode2/types.d.ts +138 -83
  15. package/dist/opencode2/types.js +8 -10
  16. package/dist/plan-execution-kickoff.d.ts +2 -0
  17. package/dist/plan-execution-kickoff.js +25 -1
  18. package/dist/plugin-opencode2.d.ts +4 -1
  19. package/dist/plugin-opencode2.js +335 -154
  20. package/dist/plugin.js +5 -1
  21. package/dist/protocol/conversation-persistence.d.ts +2 -0
  22. package/dist/protocol/conversation-persistence.js +19 -1
  23. package/dist/protocol/conversation-state.d.ts +4 -0
  24. package/dist/protocol/conversation-state.js +4 -0
  25. package/dist/protocol/switch-mode.d.ts +5 -4
  26. package/dist/protocol/switch-mode.js +12 -4
  27. package/dist/protocol/tool-call-bridge.js +1 -1
  28. package/dist/protocol/tools.d.ts +30 -4
  29. package/dist/protocol/tools.js +185 -58
  30. package/dist/session.d.ts +7 -1
  31. package/dist/shared.d.ts +3 -1
  32. package/dist/shared.js +3 -1
  33. package/dist/shell-timeout.d.ts +13 -0
  34. package/dist/shell-timeout.js +38 -0
  35. package/dist/todo-store.d.ts +29 -0
  36. package/dist/todo-store.js +78 -0
  37. package/dist/web-tools.d.ts +13 -0
  38. package/dist/web-tools.js +83 -5
  39. package/package.json +6 -3
@@ -1,16 +1,14 @@
1
1
  /**
2
- * Local structural types for the OpenCode 2.0 beta plugin API.
2
+ * Runtime duck-type boundary for the OpenCode 2.0 plugin — not host conformance.
3
3
  *
4
- * Why not import from `@opencode-ai/plugin`?
5
- * • The 2.0 types live on the `@next` dist-tag (`0.0.0-next-*`), which cannot
6
- * coexist with this package's existing `@opencode-ai/plugin@^1.17.13`
7
- * dependency under the same module specifier.
8
- * • 2.0 is beta and its schema churns; a hand-maintained subset of only what
9
- * we touch is more stable than pinning a moving target.
4
+ * Only the methods and fields this plugin calls or publishes. Extra host
5
+ * fields are ignored at runtime. Do not import `@opencode/plugin`: it is not a
6
+ * standalone types surface we can pin next to `@opencode-ai/plugin`, and a host
7
+ * dependency would force a plugin bump on every OpenCode release.
10
8
  *
11
- * These mirror `@opencode-ai/plugin@next` `dist/promise/*.d.ts` as of
12
- * `0.0.0-next-17155`. `test/opencode2-conformance.types.ts` asserts our
13
- * default export still satisfies the real 2.0 `Plugin` interface.
9
+ * Compile-time checks that our *calls and payloads* still fit the fuller host
10
+ * editor live in `test/opencode2-conformance.types.ts` against
11
+ * `test/opencode2-host-contract.ts`.
14
12
  *
15
13
  * Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain
16
14
  * `string`; brands are compile-time only and erase at runtime.
@@ -25,17 +23,12 @@ export type ProviderInfo = {
25
23
  name: string;
26
24
  /** `"aisdk:<pkg>"` selects the AI SDK path; a bare specifier selects native. */
27
25
  package: string;
26
+ activation: "auto" | "enabled" | "disabled";
28
27
  integrationID?: string;
29
- disabled?: boolean;
30
- settings?: Record<string, any>;
31
- headers?: Record<string, string>;
32
- body?: Record<string, any>;
33
28
  };
34
29
  export type ModelVariantInfo = {
35
30
  id: string;
36
31
  settings?: Record<string, any>;
37
- headers?: Record<string, string>;
38
- body?: Record<string, any>;
39
32
  };
40
33
  export type ModelInfo2 = {
41
34
  id: string;
@@ -43,7 +36,6 @@ export type ModelInfo2 = {
43
36
  modelID: string;
44
37
  providerID: string;
45
38
  name: string;
46
- family?: string;
47
39
  capabilities: {
48
40
  tools: boolean;
49
41
  input: readonly string[];
@@ -66,7 +58,7 @@ export type ModelInfo2 = {
66
58
  write: number;
67
59
  };
68
60
  }[];
69
- status: "alpha" | "beta" | "deprecated" | "active";
61
+ status: "active";
70
62
  enabled: boolean;
71
63
  limit: {
72
64
  context: number;
@@ -74,9 +66,6 @@ export type ModelInfo2 = {
74
66
  output: number;
75
67
  };
76
68
  settings?: Record<string, any>;
77
- headers?: Record<string, string>;
78
- body?: Record<string, any>;
79
- package?: string;
80
69
  };
81
70
  export type CredentialOAuth = {
82
71
  type: "oauth";
@@ -101,35 +90,18 @@ export type ConnectionInfo = {
101
90
  name: string;
102
91
  };
103
92
  /**
104
- * Read accessors we never call are intentionally loose (`any`). Modelling the
105
- * host's Effect-schema records exactly would make the conformance guard churn on
106
- * unrelated upstream edits without protecting anything we depend on. The
107
- * `update` signatures — the ones we actually build the catalog with — stay precise.
93
+ * Inventory writer this plugin uses (`editor.add` only).
94
+ * The host editor is a superset; unused methods are not part of this boundary.
108
95
  */
109
- export type CatalogDraft = {
110
- readonly provider: {
111
- list(): readonly any[];
112
- get(providerID: string): any;
113
- /** Upsert: creates the provider when absent. */
114
- update(providerID: string, update: (provider: ProviderInfo) => void): void;
115
- remove(providerID: string): void;
116
- };
117
- readonly model: {
118
- get(providerID: string, modelID: string): any;
119
- /** Upsert: creates provider and/or model when absent. */
120
- update(providerID: string, modelID: string, update: (model: ModelInfo2) => void): void;
121
- remove(providerID: string, modelID: string): void;
122
- readonly default: {
123
- get(): {
124
- providerID: string;
125
- modelID: string;
126
- } | undefined;
127
- set(providerID: string, modelID: string): void;
128
- };
129
- };
130
- };
131
- export type CatalogDomain = {
132
- readonly transform: Transform<CatalogDraft>;
96
+ export type ProviderEditor = {
97
+ add(input: {
98
+ info: ProviderInfo;
99
+ models: readonly ModelInfo2[];
100
+ sourceConnection?: ConnectionInfo;
101
+ }): void;
102
+ };
103
+ export type ProviderDomain = {
104
+ readonly transform: Transform<ProviderEditor>;
133
105
  readonly reload: () => Promise<void>;
134
106
  };
135
107
  export type IntegrationTextPrompt = {
@@ -142,6 +114,7 @@ export type IntegrationOAuthMethod = {
142
114
  id: string;
143
115
  type: "oauth";
144
116
  label: string;
117
+ form?: unknown;
145
118
  prompts?: IntegrationTextPrompt[];
146
119
  };
147
120
  export type IntegrationKeyMethod = {
@@ -167,7 +140,7 @@ export type IntegrationOAuthAuthorization = {
167
140
  export type IntegrationOAuthMethodRegistration = {
168
141
  readonly integrationID: string;
169
142
  readonly method: IntegrationOAuthMethod;
170
- readonly authorize: (inputs: Record<string, string>) => Promise<IntegrationOAuthAuthorization>;
143
+ readonly authorize: (answer: any) => Promise<IntegrationOAuthAuthorization>;
171
144
  readonly refresh?: (credential: CredentialOAuth) => Promise<CredentialOAuth>;
172
145
  readonly label?: (credential: CredentialOAuth) => string | undefined;
173
146
  };
@@ -183,14 +156,9 @@ export type IntegrationRef = {
183
156
  name: string;
184
157
  };
185
158
  export type IntegrationDraft = {
186
- list(): readonly any[];
187
- get(id: string): any;
188
159
  update(id: string, update: (integration: IntegrationRef) => void): void;
189
- remove(id: string): void;
190
160
  readonly method: {
191
- list(integrationID: string): readonly any[];
192
161
  update(input: IntegrationMethodRegistration): void;
193
- remove(integrationID: string, method: any): void;
194
162
  };
195
163
  };
196
164
  export type IntegrationDomain = {
@@ -201,15 +169,21 @@ export type IntegrationDomain = {
201
169
  readonly resolve: (connection: ConnectionInfo) => Promise<CredentialValue | undefined>;
202
170
  };
203
171
  };
172
+ /** Fields this plugin reads from the host model object. */
173
+ export type HostModelRef = {
174
+ readonly id: string;
175
+ readonly modelID: string;
176
+ readonly providerID: string;
177
+ };
204
178
  export type AISDKHooks = {
205
179
  sdk: {
206
- readonly model: ModelInfo2;
180
+ readonly model: HostModelRef;
207
181
  readonly package: string;
208
182
  readonly options: Record<string, any>;
209
183
  sdk?: any;
210
184
  };
211
185
  language: {
212
- readonly model: ModelInfo2;
186
+ readonly model: HostModelRef;
213
187
  readonly sdk: any;
214
188
  readonly options: Record<string, any>;
215
189
  language?: unknown;
@@ -218,15 +192,37 @@ export type AISDKHooks = {
218
192
  export type AISDKDomain = {
219
193
  readonly hook: Hooks<AISDKHooks>;
220
194
  };
195
+ export type ToolOptions = {
196
+ readonly namespace?: string;
197
+ readonly permission?: string;
198
+ readonly codemode?: boolean;
199
+ readonly pinned?: boolean;
200
+ };
201
+ /** Public OpenCode 2 tool context. It deliberately has no permission `ask`. */
202
+ export type ToolExecutionContext = {
203
+ readonly sessionID: string;
204
+ readonly agent: string;
205
+ readonly messageID: string;
206
+ readonly id: string;
207
+ readonly progress: (update: Record<string, unknown>) => Promise<void>;
208
+ };
221
209
  export type ToolDefinition = {
222
210
  readonly name: string;
223
211
  readonly description: string;
224
212
  readonly input: unknown;
225
213
  readonly output?: unknown;
226
- readonly execute: (input: any, context: any) => Promise<any>;
214
+ readonly options?: ToolOptions;
215
+ readonly execute: (input: any, context: ToolExecutionContext) => Promise<any>;
227
216
  };
228
217
  export type ToolDraft = {
229
218
  add(tool: ToolDefinition): void;
219
+ /** Host ToolEditor.get — used to skip registering tools the host already owns. */
220
+ get?(id: string): (ToolDefinition & {
221
+ readonly id?: string;
222
+ }) | undefined;
223
+ list?(): readonly (ToolDefinition & {
224
+ readonly id: string;
225
+ })[];
230
226
  };
231
227
  export type ToolHookBaseFields = {
232
228
  readonly tool: string;
@@ -257,6 +253,7 @@ export type ToolHooks = {
257
253
  };
258
254
  export type ToolDomain = {
259
255
  readonly transform: Transform<ToolDraft>;
256
+ readonly reload: () => Promise<void>;
260
257
  readonly hook: Hooks<ToolHooks>;
261
258
  };
262
259
  export type SessionContext = {
@@ -273,11 +270,38 @@ export type SessionContext = {
273
270
  description: string;
274
271
  input: unknown;
275
272
  }>;
273
+ /** Mutable generation / provider options. OpenCode 2.0 SessionRequest.options. */
274
+ options?: Record<string, unknown>;
275
+ };
276
+ export type SessionCompactionResult = {
277
+ summary: string;
278
+ providerState?: unknown;
279
+ metadata?: Record<string, unknown>;
280
+ tokens?: unknown;
281
+ };
282
+ export type SessionCompaction = SessionContext & {
283
+ result?: SessionCompactionResult;
284
+ };
285
+ export type SessionGenerate = SessionContext;
286
+ export type SessionTitle = {
287
+ readonly sessionID: string;
288
+ readonly model: {
289
+ providerID: string;
290
+ id: string;
291
+ variant?: string;
292
+ };
293
+ system: Array<any>;
294
+ messages: Array<any>;
295
+ options?: Record<string, unknown>;
296
+ result?: string;
276
297
  };
277
298
  export type SessionHooks = {
278
299
  readonly context: SessionContext;
300
+ readonly compaction: SessionCompaction;
301
+ readonly generate: SessionGenerate;
302
+ readonly title: SessionTitle;
279
303
  };
280
- /** Only the `location.directory` field we actually read; the rest of the real `SessionInfo` is loose. */
304
+ /** Only the `location.directory` field we actually read. */
281
305
  export type SessionInfo = {
282
306
  readonly id: string;
283
307
  readonly location: {
@@ -289,43 +313,74 @@ export type SessionDomain = {
289
313
  readonly get: (input: {
290
314
  sessionID: string;
291
315
  }) => Promise<SessionInfo>;
316
+ readonly switchAgent?: (input: {
317
+ sessionID: string;
318
+ agent: string;
319
+ }) => Promise<void>;
320
+ readonly synthetic?: (input: {
321
+ sessionID: string;
322
+ text: string;
323
+ description?: string;
324
+ metadata?: Record<string, unknown>;
325
+ delivery?: "steer" | "queue";
326
+ resume?: boolean;
327
+ }) => Promise<unknown>;
328
+ readonly prompt?: (input: {
329
+ sessionID: string;
330
+ text: string;
331
+ }) => Promise<unknown>;
292
332
  };
293
- export type WebSearchDefinition = {
294
- readonly id: string;
295
- readonly name: string;
296
- readonly execute: (input: any, context: {
297
- readonly signal: AbortSignal;
298
- }) => Promise<readonly any[]>;
299
- };
300
- export type WebSearchDraft = {
301
- add(definition: WebSearchDefinition): void;
302
- readonly default: {
303
- get(): string | undefined;
304
- set(providerID: string): void;
333
+ export type EventDomain = {
334
+ readonly subscribe: (...args: any[]) => any;
335
+ };
336
+ export type ShellCreateBefore = {
337
+ command: string;
338
+ cwd: string;
339
+ timeout: number;
340
+ shell: string;
341
+ env: Record<string, string | undefined>;
342
+ };
343
+ export type ShellDomain = {
344
+ readonly hook: Hooks<{
345
+ readonly "create.before": ShellCreateBefore;
346
+ }>;
347
+ };
348
+ export type WebSearchResult = {
349
+ url: string;
350
+ title?: string;
351
+ content?: string;
352
+ time: {
353
+ published?: number;
305
354
  };
306
355
  };
356
+ export type WebSearchEditor = {
357
+ add(definition: {
358
+ readonly id: string;
359
+ readonly name: string;
360
+ readonly execute: (input: {
361
+ query: string;
362
+ }, context: {
363
+ readonly signal: AbortSignal;
364
+ }) => Promise<readonly WebSearchResult[]>;
365
+ }): void;
366
+ };
307
367
  export type WebSearchDomain = {
308
- readonly transform: Transform<WebSearchDraft>;
368
+ readonly transform: Transform<WebSearchEditor>;
309
369
  readonly reload: () => Promise<void>;
310
370
  };
311
- export type EventDomain = {
312
- readonly subscribe: (...args: any[]) => any;
313
- };
314
- export type App = {
315
- readonly name: string;
316
- readonly version: string;
317
- readonly channel: string;
371
+ export type PluginLocation = {
372
+ readonly directory: string;
318
373
  };
319
374
  export type PluginContext = {
320
- readonly app: App;
321
- readonly options: Readonly<Record<string, any>>;
322
375
  readonly aisdk: AISDKDomain;
323
- readonly catalog: CatalogDomain;
324
376
  readonly event: EventDomain;
325
377
  readonly integration: IntegrationDomain;
378
+ readonly provider: ProviderDomain;
326
379
  readonly session: SessionDomain;
327
380
  readonly tool: ToolDomain;
328
- readonly websearch: WebSearchDomain;
381
+ readonly location?: PluginLocation;
382
+ readonly shell?: ShellDomain;
383
+ readonly websearch?: WebSearchDomain;
329
384
  };
330
385
  export type Cleanup = () => Promise<void> | void;
331
386
  export type Plugin2 = {
@@ -1,16 +1,14 @@
1
1
  /**
2
- * Local structural types for the OpenCode 2.0 beta plugin API.
2
+ * Runtime duck-type boundary for the OpenCode 2.0 plugin — not host conformance.
3
3
  *
4
- * Why not import from `@opencode-ai/plugin`?
5
- * • The 2.0 types live on the `@next` dist-tag (`0.0.0-next-*`), which cannot
6
- * coexist with this package's existing `@opencode-ai/plugin@^1.17.13`
7
- * dependency under the same module specifier.
8
- * • 2.0 is beta and its schema churns; a hand-maintained subset of only what
9
- * we touch is more stable than pinning a moving target.
4
+ * Only the methods and fields this plugin calls or publishes. Extra host
5
+ * fields are ignored at runtime. Do not import `@opencode/plugin`: it is not a
6
+ * standalone types surface we can pin next to `@opencode-ai/plugin`, and a host
7
+ * dependency would force a plugin bump on every OpenCode release.
10
8
  *
11
- * These mirror `@opencode-ai/plugin@next` `dist/promise/*.d.ts` as of
12
- * `0.0.0-next-17155`. `test/opencode2-conformance.types.ts` asserts our
13
- * default export still satisfies the real 2.0 `Plugin` interface.
9
+ * Compile-time checks that our *calls and payloads* still fit the fuller host
10
+ * editor live in `test/opencode2-conformance.types.ts` against
11
+ * `test/opencode2-host-contract.ts`.
14
12
  *
15
13
  * Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain
16
14
  * `string`; brands are compile-time only and erase at runtime.
@@ -37,6 +37,8 @@ export declare function setPlanExecutionKickoff(fn: PlanExecutionKickoffFn | und
37
37
  export declare function hasPlanExecutionKickoff(): boolean;
38
38
  /** Record the approved plan; execution is flushed after the stream settles. */
39
39
  export declare function queuePlanExecutionKickoff(input: PlanExecutionKickoffInput): boolean;
40
+ /** Drop any approval state owned by a deleted host session. */
41
+ export declare function cancelPlanExecutionKickoff(sessionID: string | undefined): void;
40
42
  /**
41
43
  * Run one pending kickoff only after the owning Cursor Run reached a terminal,
42
44
  * idle state. Returns true only after successful host handoff.
@@ -53,9 +53,14 @@ export function planPathFromUri(planUri) {
53
53
  let kickoff;
54
54
  const pending = new Map();
55
55
  const warnings = new Map();
56
+ const MAX_PENDING_PLAN_KICKOFFS = 256;
56
57
  /** Install (or clear) the host kickoff. */
57
58
  export function setPlanExecutionKickoff(fn) {
58
59
  kickoff = fn;
60
+ if (!fn) {
61
+ pending.clear();
62
+ warnings.clear();
63
+ }
59
64
  }
60
65
  export function hasPlanExecutionKickoff() {
61
66
  return kickoff !== undefined;
@@ -77,10 +82,25 @@ export function queuePlanExecutionKickoff(input) {
77
82
  attempts: 0,
78
83
  });
79
84
  warnings.delete(sessionID);
85
+ while (pending.size > MAX_PENDING_PLAN_KICKOFFS) {
86
+ const oldest = pending.keys().next().value;
87
+ if (!oldest)
88
+ break;
89
+ pending.delete(oldest);
90
+ warnings.delete(oldest);
91
+ }
80
92
  trace(`plan-execution-kickoff: pending sessionID=${sessionID} ` +
81
93
  `cursorSessionID=${input.cursorSessionID ?? ""} planPath=${planPath}`);
82
94
  return true;
83
95
  }
96
+ /** Drop any approval state owned by a deleted host session. */
97
+ export function cancelPlanExecutionKickoff(sessionID) {
98
+ const key = sessionID?.trim();
99
+ if (!key)
100
+ return;
101
+ pending.delete(key);
102
+ warnings.delete(key);
103
+ }
84
104
  /**
85
105
  * Run one pending kickoff only after the owning Cursor Run reached a terminal,
86
106
  * idle state. Returns true only after successful host handoff.
@@ -100,7 +120,11 @@ export async function flushPlanExecutionKickoff(sessionID, options = {}) {
100
120
  if (state.cursorSessionID
101
121
  && options.cursorSessionID
102
122
  && state.cursorSessionID !== options.cursorSessionID) {
103
- trace(`plan-execution-kickoff: skipped stale Run sessionID=${key} ` +
123
+ // The owning Run was superseded before the handoff could happen. Applying
124
+ // it from a later Run would execute a stale approval in the wrong turn.
125
+ pending.delete(key);
126
+ warnings.delete(key);
127
+ trace(`plan-execution-kickoff: discarded stale Run sessionID=${key} ` +
104
128
  `owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`);
105
129
  return false;
106
130
  }
@@ -1,3 +1,6 @@
1
+ import { CursorPlugin } from "./plugin.js";
1
2
  import type { Plugin2 } from "./opencode2/types.js";
2
- declare const plugin: Plugin2;
3
+ declare const plugin: Plugin2 & {
4
+ server: typeof CursorPlugin;
5
+ };
3
6
  export default plugin;