opencode-overclock 0.3.0 → 0.4.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.
- package/README.md +206 -111
- package/package.json +2 -2
- package/src/bridge.ts +1 -0
- package/src/buddy/companion.ts +104 -5
- package/src/buddy/sprites.ts +4 -4
- package/src/buddy/tui.ts +175 -65
- package/src/core/bridge.ts +34 -0
- package/src/core/lifecycle.ts +53 -0
- package/src/core/policy.ts +128 -0
- package/src/core/summary.ts +33 -0
- package/src/core/types.ts +164 -0
- package/src/features/buddy.ts +1 -2
- package/src/features/guard.ts +168 -30
- package/src/features/index.ts +6 -4
- package/src/features/recovery.ts +143 -0
- package/src/features/sched.ts +147 -89
- package/src/features/tasks.ts +52 -18
- package/src/features/truncator.ts +99 -0
- package/src/features/usage.ts +26 -65
- package/src/index.ts +96 -67
- package/src/lib/busy.ts +1 -25
- package/src/lib/exec.ts +7 -0
- package/src/lib/inject.ts +10 -56
- package/src/lib/mirror.ts +13 -0
- package/src/lib/probe.ts +1 -15
- package/src/lib/state.ts +10 -39
- package/src/lib/tmux.ts +1 -0
- package/src/lib/ui.ts +208 -0
- package/src/merge.ts +2 -66
- package/src/platform/probe.ts +25 -0
- package/src/platform/process/exec.ts +76 -0
- package/src/platform/process/tmux.ts +60 -0
- package/src/platform/session/busy.ts +33 -0
- package/src/platform/session/inject.ts +82 -0
- package/src/platform/session/notify.ts +20 -0
- package/src/platform/storage/state.ts +77 -0
- package/src/platform/storage/store.ts +61 -0
- package/src/summary.ts +1 -0
- package/src/tools.ts +8 -244
- package/src/tui.ts +57 -186
- package/src/types.ts +1 -73
- package/src/v2/context.ts +470 -0
- package/src/v2/host.ts +117 -0
- package/src/v2/loader.ts +150 -0
- package/src/buddy/reactions.ts +0 -41
- package/src/buddy/types.ts +0 -30
- package/src/config.ts +0 -19
- package/src/features/checkpoints.ts +0 -128
- package/src/features/sandbox.ts +0 -104
- package/src/validate.ts +0 -197
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview OpenCode V2 Synthetic Context Bridge
|
|
3
|
+
*
|
|
4
|
+
* OpenCode V1 runtime environments only load plugins via `server(input, options)`
|
|
5
|
+
* returning an imperative `Hooks` object. OpenCode V2 introduces a declarative,
|
|
6
|
+
* transform-based domain architecture (`setup(context)`) where plugins register
|
|
7
|
+
* mutations on domain drafts (`agent`, `command`, `catalog`, `skill`, `reference`,
|
|
8
|
+
* `aisdk`, `plugin`).
|
|
9
|
+
*
|
|
10
|
+
* This module synthesizes a complete, spec-conforming `PluginContext` in memory on V1 hosts:
|
|
11
|
+
*
|
|
12
|
+
* - `agent.transform` --> Mutates `config.agent` and `config.default_agent` in V1 `config` hook.
|
|
13
|
+
* - `command.transform` --> Mutates `config.command` dictionary in V1 `config` hook.
|
|
14
|
+
* - `catalog.transform` --> Mutates `config.provider` and `config.model` in V1 `config` hook.
|
|
15
|
+
* - `skill.transform` --> Collects external skill sources into memory.
|
|
16
|
+
* - `reference.transform` --> Collects reference sources injected into `experimental.chat.system.transform`.
|
|
17
|
+
* - `aisdk.sdk` --> Intercepts LLM parameters via `chat.params` hook (`language` collected for spec compatibility).
|
|
18
|
+
* - `plugin.add / remove` --> Manages nested V2 plugins with scope-owned resource disposal.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
AgentDraft,
|
|
23
|
+
AgentHooks,
|
|
24
|
+
AISDKHooks,
|
|
25
|
+
CatalogDraft,
|
|
26
|
+
CatalogHooks,
|
|
27
|
+
CatalogProviderRecord,
|
|
28
|
+
CommandDraft,
|
|
29
|
+
CommandHooks,
|
|
30
|
+
IntegrationDraft,
|
|
31
|
+
IntegrationHooks,
|
|
32
|
+
Plugin as V2Plugin,
|
|
33
|
+
PluginContext,
|
|
34
|
+
PluginDomain,
|
|
35
|
+
PluginOptions,
|
|
36
|
+
ReferenceDraft,
|
|
37
|
+
ReferenceHooks,
|
|
38
|
+
Registration,
|
|
39
|
+
Reload,
|
|
40
|
+
SkillDraft,
|
|
41
|
+
SkillHooks,
|
|
42
|
+
} from "@opencode-ai/plugin/v2/promise"
|
|
43
|
+
import type {
|
|
44
|
+
AgentV2Info,
|
|
45
|
+
CommandV2Info,
|
|
46
|
+
ModelV2Info,
|
|
47
|
+
ProviderV2Info,
|
|
48
|
+
ReferenceGitSource,
|
|
49
|
+
ReferenceLocalSource,
|
|
50
|
+
SkillV2Source,
|
|
51
|
+
} from "@opencode-ai/sdk/v2/types"
|
|
52
|
+
|
|
53
|
+
/** Disposer callback returned from registration or plugin lifecycle */
|
|
54
|
+
export type Disposer = () => Promise<void> | void
|
|
55
|
+
|
|
56
|
+
/** Internal state maintaining all active V2 transform registrations and collected metadata */
|
|
57
|
+
export interface V2RegistryState {
|
|
58
|
+
agentTransforms: Set<(draft: AgentDraft) => Promise<void> | void>
|
|
59
|
+
commandTransforms: Set<(draft: CommandDraft) => Promise<void> | void>
|
|
60
|
+
skillTransforms: Set<(draft: SkillDraft) => Promise<void> | void>
|
|
61
|
+
catalogTransforms: Set<(draft: CatalogDraft) => Promise<void> | void>
|
|
62
|
+
referenceTransforms: Set<(draft: ReferenceDraft) => Promise<void> | void>
|
|
63
|
+
integrationTransforms: Set<(draft: IntegrationDraft) => Promise<void> | void>
|
|
64
|
+
aisdkHooks: {
|
|
65
|
+
sdk: Set<(input: any) => Promise<void> | void>
|
|
66
|
+
language: Set<(input: any) => Promise<void> | void>
|
|
67
|
+
}
|
|
68
|
+
references: Map<string, ReferenceLocalSource | ReferenceGitSource>
|
|
69
|
+
skillSources: SkillV2Source[]
|
|
70
|
+
activePlugins: Map<string, { plugin: V2Plugin; disposers: Set<Disposer> }>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Operational handle exposing the synthetic context and configuration transform application */
|
|
74
|
+
export interface V2ContextHandle {
|
|
75
|
+
/** The root PluginContext passed to V2 setup() */
|
|
76
|
+
context: PluginContext
|
|
77
|
+
/** Internal transform and plugin state */
|
|
78
|
+
state: V2RegistryState
|
|
79
|
+
/** Applies all registered V2 transforms against the host's V1 config object */
|
|
80
|
+
applyConfigTransforms(cfg: Record<string, any>): Promise<void>
|
|
81
|
+
/** Creates a scoped child PluginContext for a specific plugin or options block */
|
|
82
|
+
scopedContext(options?: PluginOptions, tracker?: Set<Disposer>): PluginContext
|
|
83
|
+
/** Cleans up all registered transforms and active plugins */
|
|
84
|
+
dispose(): Promise<void>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Wraps a disposer into a V2 Registration object, tracking it in an optional scope set */
|
|
88
|
+
function makeRegistration(disposer: Disposer, tracker?: Set<Disposer>): Promise<Registration> {
|
|
89
|
+
const wrappedDisposer = async () => {
|
|
90
|
+
tracker?.delete(disposer)
|
|
91
|
+
await disposer()
|
|
92
|
+
}
|
|
93
|
+
tracker?.add(disposer)
|
|
94
|
+
return Promise.resolve({ dispose: wrappedDisposer })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Generic factory for transform-based domains implementing `transform(cb)` + `Reload` */
|
|
98
|
+
function createTransformDomain<TDraft>(
|
|
99
|
+
transforms: Set<(draft: TDraft) => Promise<void> | void>,
|
|
100
|
+
reload: Reload,
|
|
101
|
+
tracker?: Set<Disposer>,
|
|
102
|
+
) {
|
|
103
|
+
return {
|
|
104
|
+
transform: (callback: (draft: TDraft) => Promise<void> | void) => {
|
|
105
|
+
transforms.add(callback)
|
|
106
|
+
return makeRegistration(() => {
|
|
107
|
+
transforms.delete(callback)
|
|
108
|
+
}, tracker)
|
|
109
|
+
},
|
|
110
|
+
...reload,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a synthetic PluginContext implementing OpenCode V2 plugin specifications.
|
|
116
|
+
* Allows V2 plugins and hybrid plugins to run inside OpenCode V1 runtime sessions.
|
|
117
|
+
*/
|
|
118
|
+
export function createV2PluginContext(
|
|
119
|
+
initialOptions: PluginOptions = {},
|
|
120
|
+
onReload?: () => Promise<void>,
|
|
121
|
+
): V2ContextHandle {
|
|
122
|
+
const state: V2RegistryState = {
|
|
123
|
+
agentTransforms: new Set(),
|
|
124
|
+
commandTransforms: new Set(),
|
|
125
|
+
skillTransforms: new Set(),
|
|
126
|
+
catalogTransforms: new Set(),
|
|
127
|
+
referenceTransforms: new Set(),
|
|
128
|
+
integrationTransforms: new Set(),
|
|
129
|
+
aisdkHooks: { sdk: new Set(), language: new Set() },
|
|
130
|
+
references: new Map(),
|
|
131
|
+
skillSources: [],
|
|
132
|
+
activePlugins: new Map(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const reloadHandler: Reload = {
|
|
136
|
+
reload: async () => {
|
|
137
|
+
if (onReload) await onReload()
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createScopedContext(
|
|
142
|
+
scopedOptions: PluginOptions = initialOptions,
|
|
143
|
+
tracker?: Set<Disposer>,
|
|
144
|
+
): PluginContext {
|
|
145
|
+
const agent = createTransformDomain(state.agentTransforms, reloadHandler, tracker)
|
|
146
|
+
const command = createTransformDomain(state.commandTransforms, reloadHandler, tracker)
|
|
147
|
+
const skill = createTransformDomain(state.skillTransforms, reloadHandler, tracker)
|
|
148
|
+
const catalog = createTransformDomain(state.catalogTransforms, reloadHandler, tracker)
|
|
149
|
+
const reference = createTransformDomain(state.referenceTransforms, reloadHandler, tracker)
|
|
150
|
+
|
|
151
|
+
const integration: IntegrationHooks & Reload = {
|
|
152
|
+
...createTransformDomain(state.integrationTransforms, reloadHandler, tracker),
|
|
153
|
+
connection: {
|
|
154
|
+
active: async () => undefined,
|
|
155
|
+
resolve: async () => undefined,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const aisdk: AISDKHooks = {
|
|
160
|
+
sdk: (callback) => {
|
|
161
|
+
state.aisdkHooks.sdk.add(callback)
|
|
162
|
+
return makeRegistration(() => {
|
|
163
|
+
state.aisdkHooks.sdk.delete(callback)
|
|
164
|
+
}, tracker)
|
|
165
|
+
},
|
|
166
|
+
language: (callback) => {
|
|
167
|
+
state.aisdkHooks.language.add(callback)
|
|
168
|
+
return makeRegistration(() => {
|
|
169
|
+
state.aisdkHooks.language.delete(callback)
|
|
170
|
+
}, tracker)
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pluginDomain: PluginDomain = {
|
|
175
|
+
add: async (p: V2Plugin) => {
|
|
176
|
+
if (state.activePlugins.has(p.id)) {
|
|
177
|
+
await pluginDomain.remove(p.id)
|
|
178
|
+
}
|
|
179
|
+
const pluginDisposers = new Set<Disposer>()
|
|
180
|
+
state.activePlugins.set(p.id, { plugin: p, disposers: pluginDisposers })
|
|
181
|
+
|
|
182
|
+
const pluginContext = createScopedContext(scopedOptions, pluginDisposers)
|
|
183
|
+
|
|
184
|
+
// Support effect-based v2 plugins if present, otherwise promise-based setup
|
|
185
|
+
if (typeof (p as any).effect === "function") {
|
|
186
|
+
const { runPromise } = await import("effect/Effect")
|
|
187
|
+
await runPromise((p as any).effect(pluginContext))
|
|
188
|
+
} else if (typeof p.setup === "function") {
|
|
189
|
+
await p.setup(pluginContext)
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
remove: async (id: string) => {
|
|
193
|
+
const entry = state.activePlugins.get(id)
|
|
194
|
+
if (!entry) return
|
|
195
|
+
for (const disposer of entry.disposers) {
|
|
196
|
+
try {
|
|
197
|
+
await disposer()
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.warn(`[overclock] v2: error disposing registration for plugin ${id}: ${e}`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
state.activePlugins.delete(id)
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
options: scopedOptions,
|
|
208
|
+
agent,
|
|
209
|
+
aisdk,
|
|
210
|
+
catalog,
|
|
211
|
+
command,
|
|
212
|
+
integration,
|
|
213
|
+
plugin: pluginDomain,
|
|
214
|
+
reference,
|
|
215
|
+
skill,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rootContext = createScopedContext(initialOptions)
|
|
220
|
+
|
|
221
|
+
async function applyConfigTransforms(cfg: Record<string, any>): Promise<void> {
|
|
222
|
+
if (!cfg || typeof cfg !== "object") return
|
|
223
|
+
await applyAgentTransforms(cfg, state.agentTransforms)
|
|
224
|
+
await applyCommandTransforms(cfg, state.commandTransforms)
|
|
225
|
+
await applyCatalogTransforms(cfg, state.catalogTransforms)
|
|
226
|
+
await applySkillTransforms(state.skillSources, state.skillTransforms)
|
|
227
|
+
await applyReferenceTransforms(state.references, state.referenceTransforms)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
context: rootContext,
|
|
232
|
+
state,
|
|
233
|
+
applyConfigTransforms,
|
|
234
|
+
scopedContext: createScopedContext,
|
|
235
|
+
dispose: async () => {
|
|
236
|
+
for (const [id, entry] of state.activePlugins) {
|
|
237
|
+
for (const disposer of entry.disposers) {
|
|
238
|
+
try {
|
|
239
|
+
await disposer()
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.warn(`[overclock] v2: error disposing ${id}: ${e}`)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
state.activePlugins.clear()
|
|
246
|
+
state.agentTransforms.clear()
|
|
247
|
+
state.commandTransforms.clear()
|
|
248
|
+
state.catalogTransforms.clear()
|
|
249
|
+
state.skillTransforms.clear()
|
|
250
|
+
state.referenceTransforms.clear()
|
|
251
|
+
state.integrationTransforms.clear()
|
|
252
|
+
state.aisdkHooks.sdk.clear()
|
|
253
|
+
state.aisdkHooks.language.clear()
|
|
254
|
+
state.references.clear()
|
|
255
|
+
state.skillSources = []
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Applies registered AgentDraft transforms onto `cfg.agent` and `cfg.default_agent` */
|
|
261
|
+
async function applyAgentTransforms(
|
|
262
|
+
cfg: Record<string, any>,
|
|
263
|
+
transforms: Set<(draft: AgentDraft) => Promise<void> | void>,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
if (transforms.size === 0) return
|
|
266
|
+
cfg.agent = cfg.agent ?? {}
|
|
267
|
+
|
|
268
|
+
const draft: AgentDraft = {
|
|
269
|
+
list: () =>
|
|
270
|
+
Object.entries(cfg.agent).map(([id, val]: [string, any]) => ({
|
|
271
|
+
id,
|
|
272
|
+
mode: val?.mode ?? "all",
|
|
273
|
+
hidden: val?.hidden ?? false,
|
|
274
|
+
permissions: val?.permissions ?? [],
|
|
275
|
+
request: val?.request ?? {},
|
|
276
|
+
...val,
|
|
277
|
+
})) as readonly AgentV2Info[],
|
|
278
|
+
get: (id) => {
|
|
279
|
+
const val = cfg.agent[id]
|
|
280
|
+
if (!val) return undefined
|
|
281
|
+
return {
|
|
282
|
+
id,
|
|
283
|
+
mode: val.mode ?? "all",
|
|
284
|
+
hidden: val.hidden ?? false,
|
|
285
|
+
permissions: val.permissions ?? [],
|
|
286
|
+
request: val.request ?? {},
|
|
287
|
+
...val,
|
|
288
|
+
} as AgentV2Info
|
|
289
|
+
},
|
|
290
|
+
default: (id) => {
|
|
291
|
+
if (id) cfg.default_agent = id
|
|
292
|
+
else delete cfg.default_agent
|
|
293
|
+
},
|
|
294
|
+
update: (id, updater) => {
|
|
295
|
+
const current = draft.get(id) ?? {
|
|
296
|
+
id,
|
|
297
|
+
mode: "all",
|
|
298
|
+
hidden: false,
|
|
299
|
+
permissions: [],
|
|
300
|
+
request: {} as any,
|
|
301
|
+
}
|
|
302
|
+
updater(current)
|
|
303
|
+
cfg.agent[id] = { ...current }
|
|
304
|
+
},
|
|
305
|
+
remove: (id) => {
|
|
306
|
+
delete cfg.agent[id]
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const transform of transforms) await transform(draft)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Applies registered CommandDraft transforms onto `cfg.command` */
|
|
314
|
+
async function applyCommandTransforms(
|
|
315
|
+
cfg: Record<string, any>,
|
|
316
|
+
transforms: Set<(draft: CommandDraft) => Promise<void> | void>,
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
if (transforms.size === 0) return
|
|
319
|
+
cfg.command = cfg.command ?? {}
|
|
320
|
+
|
|
321
|
+
const draft: CommandDraft = {
|
|
322
|
+
list: () =>
|
|
323
|
+
Object.entries(cfg.command).map(([name, val]: [string, any]) => ({
|
|
324
|
+
name,
|
|
325
|
+
template: val?.template ?? "",
|
|
326
|
+
description: val?.description,
|
|
327
|
+
agent: val?.agent,
|
|
328
|
+
model: val?.model,
|
|
329
|
+
subtask: val?.subtask,
|
|
330
|
+
...val,
|
|
331
|
+
})) as readonly CommandV2Info[],
|
|
332
|
+
get: (name) => {
|
|
333
|
+
const val = cfg.command[name]
|
|
334
|
+
if (!val) return undefined
|
|
335
|
+
return {
|
|
336
|
+
name,
|
|
337
|
+
template: val.template ?? "",
|
|
338
|
+
description: val.description,
|
|
339
|
+
agent: val.agent,
|
|
340
|
+
model: val.model,
|
|
341
|
+
subtask: val.subtask,
|
|
342
|
+
...val,
|
|
343
|
+
} as CommandV2Info
|
|
344
|
+
},
|
|
345
|
+
update: (name, updater) => {
|
|
346
|
+
const current = draft.get(name) ?? { name, template: "" }
|
|
347
|
+
updater(current)
|
|
348
|
+
cfg.command[name] = { ...current }
|
|
349
|
+
},
|
|
350
|
+
remove: (name) => {
|
|
351
|
+
delete cfg.command[name]
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const transform of transforms) await transform(draft)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Applies registered CatalogDraft transforms onto `cfg.provider` and `cfg.model` */
|
|
359
|
+
async function applyCatalogTransforms(
|
|
360
|
+
cfg: Record<string, any>,
|
|
361
|
+
transforms: Set<(draft: CatalogDraft) => Promise<void> | void>,
|
|
362
|
+
): Promise<void> {
|
|
363
|
+
if (transforms.size === 0) return
|
|
364
|
+
cfg.provider = cfg.provider ?? {}
|
|
365
|
+
|
|
366
|
+
const draft: CatalogDraft = {
|
|
367
|
+
provider: {
|
|
368
|
+
list: () =>
|
|
369
|
+
Object.entries(cfg.provider).map(([providerID, val]: [string, any]) => ({
|
|
370
|
+
provider: { id: providerID, name: val?.name ?? providerID, ...val } as ProviderV2Info,
|
|
371
|
+
models: new Map(
|
|
372
|
+
Object.entries(val?.models ?? {}).map(([mID, mVal]: [string, any]) => [
|
|
373
|
+
mID,
|
|
374
|
+
{ id: mID, providerID, ...mVal } as ModelV2Info,
|
|
375
|
+
]),
|
|
376
|
+
),
|
|
377
|
+
})),
|
|
378
|
+
get: (providerID) => {
|
|
379
|
+
const val = cfg.provider[providerID]
|
|
380
|
+
if (!val) return undefined
|
|
381
|
+
return {
|
|
382
|
+
provider: { id: providerID, name: val.name ?? providerID, ...val } as ProviderV2Info,
|
|
383
|
+
models: new Map(
|
|
384
|
+
Object.entries(val.models ?? {}).map(([mID, mVal]: [string, any]) => [
|
|
385
|
+
mID,
|
|
386
|
+
{ id: mID, providerID, ...mVal } as ModelV2Info,
|
|
387
|
+
]),
|
|
388
|
+
),
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
update: (providerID, updater) => {
|
|
392
|
+
const current = (cfg.provider[providerID] ?? {
|
|
393
|
+
id: providerID,
|
|
394
|
+
name: providerID,
|
|
395
|
+
}) as ProviderV2Info
|
|
396
|
+
updater(current)
|
|
397
|
+
cfg.provider[providerID] = { ...cfg.provider[providerID], ...current }
|
|
398
|
+
},
|
|
399
|
+
remove: (providerID) => {
|
|
400
|
+
delete cfg.provider[providerID]
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
model: {
|
|
404
|
+
get: (providerID, modelID) => {
|
|
405
|
+
const p = cfg.provider[providerID]
|
|
406
|
+
const m = p?.models?.[modelID]
|
|
407
|
+
return m ? ({ id: modelID, providerID, ...m } as ModelV2Info) : undefined
|
|
408
|
+
},
|
|
409
|
+
update: (providerID, modelID, updater) => {
|
|
410
|
+
cfg.provider[providerID] = cfg.provider[providerID] ?? {}
|
|
411
|
+
cfg.provider[providerID].models = cfg.provider[providerID].models ?? {}
|
|
412
|
+
const current = (cfg.provider[providerID].models[modelID] ?? {
|
|
413
|
+
id: modelID,
|
|
414
|
+
providerID,
|
|
415
|
+
}) as ModelV2Info
|
|
416
|
+
updater(current)
|
|
417
|
+
cfg.provider[providerID].models[modelID] = { ...current }
|
|
418
|
+
},
|
|
419
|
+
remove: (providerID, modelID) => {
|
|
420
|
+
if (cfg.provider[providerID]?.models) {
|
|
421
|
+
delete cfg.provider[providerID].models[modelID]
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
default: {
|
|
425
|
+
get: () => {
|
|
426
|
+
if (typeof cfg.model === "string") {
|
|
427
|
+
const [p, ...rest] = cfg.model.split("/")
|
|
428
|
+
return rest.length ? { providerID: p, modelID: rest.join("/") } : undefined
|
|
429
|
+
}
|
|
430
|
+
if (typeof cfg.model === "object" && cfg.model) {
|
|
431
|
+
return { providerID: cfg.model.providerID, modelID: cfg.model.modelID }
|
|
432
|
+
}
|
|
433
|
+
return undefined
|
|
434
|
+
},
|
|
435
|
+
set: (providerID, modelID) => {
|
|
436
|
+
cfg.model = `${providerID}/${modelID}`
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const transform of transforms) await transform(draft)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Collects skill sources from registered SkillDraft transforms */
|
|
446
|
+
async function applySkillTransforms(
|
|
447
|
+
skillSources: SkillV2Source[],
|
|
448
|
+
transforms: Set<(draft: SkillDraft) => Promise<void> | void>,
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
if (transforms.size === 0) return
|
|
451
|
+
const draft: SkillDraft = {
|
|
452
|
+
source: (src) => skillSources.push(src),
|
|
453
|
+
list: () => skillSources,
|
|
454
|
+
}
|
|
455
|
+
for (const transform of transforms) await transform(draft)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Collects reference sources from registered ReferenceDraft transforms */
|
|
459
|
+
async function applyReferenceTransforms(
|
|
460
|
+
references: Map<string, ReferenceLocalSource | ReferenceGitSource>,
|
|
461
|
+
transforms: Set<(draft: ReferenceDraft) => Promise<void> | void>,
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
if (transforms.size === 0) return
|
|
464
|
+
const draft: ReferenceDraft = {
|
|
465
|
+
add: (name, source) => references.set(name, source),
|
|
466
|
+
remove: (name) => references.delete(name),
|
|
467
|
+
list: () => [...references.entries()],
|
|
468
|
+
}
|
|
469
|
+
for (const transform of transforms) await transform(draft)
|
|
470
|
+
}
|
package/src/v2/host.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview OpenCode V2 Host Engine
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the execution of OpenCode V2 plugins inside an OpenCode V1 runtime session.
|
|
5
|
+
* Manages plugin loading, setup invocation, and generates the V1 `Hooks` that integrate
|
|
6
|
+
* V2 domain transforms into the host's lifecycle:
|
|
7
|
+
*
|
|
8
|
+
* - `config`: Executes `agent`, `command`, `catalog`, `skill`, and `reference` draft transforms against host configuration and context state.
|
|
9
|
+
* - `experimental.chat.system.transform`: Injects reference documents into the system prompt.
|
|
10
|
+
* - `chat.params`: Forwards model/provider parameters to registered AI-SDK middleware hooks.
|
|
11
|
+
* - `dispose`: Gracefully tears down all active V2 plugins and their scoped registrations.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
|
15
|
+
import type { PluginContext } from "@opencode-ai/plugin/v2/promise"
|
|
16
|
+
import { createV2PluginContext, type V2ContextHandle } from "./context.ts"
|
|
17
|
+
import { loadV2Plugin, type V2PluginSpec } from "./loader.ts"
|
|
18
|
+
import type { OverclockOptions } from "../types.ts"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Controller interface for running V2 plugins on a V1 host.
|
|
22
|
+
*/
|
|
23
|
+
export interface V2Host {
|
|
24
|
+
/** The low-level synthetic context handle */
|
|
25
|
+
handle: V2ContextHandle
|
|
26
|
+
/** The root PluginContext passed to V2 setup methods */
|
|
27
|
+
context: PluginContext
|
|
28
|
+
/** Loads and initializes a list of V2 plugin specifiers */
|
|
29
|
+
loadPlugins(specs?: V2PluginSpec[]): Promise<string[]>
|
|
30
|
+
/** Executes a feature's or hybrid plugin's V2 setup function */
|
|
31
|
+
runSetup(
|
|
32
|
+
setupFn: (ctx: PluginContext, opts?: Record<string, unknown>) => Promise<void> | void,
|
|
33
|
+
opts?: Record<string, unknown>,
|
|
34
|
+
): Promise<void>
|
|
35
|
+
/** Creates V1 Hooks integrating all V2 transforms into the OpenCode V1 engine */
|
|
36
|
+
createHooks(): Partial<Hooks>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a V2Host instance that adapts OpenCode V2 plugin transforms
|
|
41
|
+
* into standard OpenCode V1 runtime Hooks.
|
|
42
|
+
*/
|
|
43
|
+
export function createV2Host(ctx: PluginInput, options: OverclockOptions = {}): V2Host {
|
|
44
|
+
const handle = createV2PluginContext(options)
|
|
45
|
+
|
|
46
|
+
const host: V2Host = {
|
|
47
|
+
handle,
|
|
48
|
+
context: handle.context,
|
|
49
|
+
|
|
50
|
+
loadPlugins: async (specs: V2PluginSpec[] = []) => {
|
|
51
|
+
const loaded: string[] = []
|
|
52
|
+
for (const spec of specs) {
|
|
53
|
+
const id = await loadV2Plugin(spec, ctx.directory, handle)
|
|
54
|
+
if (id) loaded.push(id)
|
|
55
|
+
}
|
|
56
|
+
return loaded
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
runSetup: async (setupFn, opts = {}) => {
|
|
60
|
+
try {
|
|
61
|
+
await setupFn(handle.context, opts)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.warn(`[overclock] v2: setup execution failed: ${e}`)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
createHooks: (): Partial<Hooks> => {
|
|
68
|
+
const hooks: Partial<Hooks> = {
|
|
69
|
+
config: async (cfg: any) => {
|
|
70
|
+
await handle.applyConfigTransforms(cfg)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
74
|
+
if (handle.state.references.size === 0) return
|
|
75
|
+
const refLines: string[] = ["# References"]
|
|
76
|
+
for (const [name, ref] of handle.state.references) {
|
|
77
|
+
if (ref.type === "local") {
|
|
78
|
+
refLines.push(`- ${name}: ${ref.path}${ref.description ? ` (${ref.description})` : ""}`)
|
|
79
|
+
} else if (ref.type === "git") {
|
|
80
|
+
refLines.push(
|
|
81
|
+
`- ${name}: ${ref.repository} (${ref.branch ?? "HEAD"})${ref.description ? ` (${ref.description})` : ""}`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
output.system.push(refLines.join("\n"))
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
"chat.params": async (input, output) => {
|
|
89
|
+
if (handle.state.aisdkHooks.sdk.size === 0) return
|
|
90
|
+
const sdkPayload = {
|
|
91
|
+
model: {
|
|
92
|
+
id: (input.model as any)?.id ?? (input.model as any)?.name ?? "unknown",
|
|
93
|
+
providerID: (input.provider as any)?.id ?? "unknown",
|
|
94
|
+
},
|
|
95
|
+
package: "@ai-sdk/provider",
|
|
96
|
+
options: output.options ?? {},
|
|
97
|
+
}
|
|
98
|
+
for (const hook of handle.state.aisdkHooks.sdk) {
|
|
99
|
+
try {
|
|
100
|
+
await hook(sdkPayload)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.warn(`[overclock] v2: aisdk.sdk hook failed: ${e}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
dispose: async () => {
|
|
108
|
+
await handle.dispose()
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return hooks
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return host
|
|
117
|
+
}
|