feat(core): manage configurable plugin generations
This commit is contained in:
parent
f9d1d3b259
commit
a9b7bd9e2f
83 changed files with 1116 additions and 819 deletions
|
|
@ -102,7 +102,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
||||||
description: "Named local directories or Git repositories available as external context",
|
description: "Named local directories or Git repositories available as external context",
|
||||||
}),
|
}),
|
||||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||||
description: "Ordered external plugin packages to load",
|
description: "Ordered plugin enablement directives and external package declarations",
|
||||||
}),
|
}),
|
||||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as ConfigAgentPlugin from "./agent"
|
export * as ConfigAgentPlugin from "./agent"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema, Stream } from "effect"
|
import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { AgentV2 } from "../../agent"
|
import { AgentV2 } from "../../agent"
|
||||||
|
|
@ -34,7 +34,7 @@ const agentKeys = new Set([
|
||||||
])
|
])
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "config-agent",
|
id: "opencode.config.agent",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as ConfigCommandPlugin from "./command"
|
export * as ConfigCommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema, Stream } from "effect"
|
import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { CommandV2 } from "../../command"
|
import { CommandV2 } from "../../command"
|
||||||
|
|
@ -13,7 +13,7 @@ import { ConfigMarkdown } from "../markdown"
|
||||||
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
|
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "config-command",
|
id: "opencode.config.command",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
export * as ConfigExternalPlugin from "./external"
|
|
||||||
|
|
||||||
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
|
||||||
import { Effect, Schema } from "effect"
|
|
||||||
import path from "path"
|
|
||||||
import { fileURLToPath, pathToFileURL } from "url"
|
|
||||||
import { Config } from "../../config"
|
|
||||||
import { FSUtil } from "../../fs-util"
|
|
||||||
import { Location } from "../../location"
|
|
||||||
import { Npm } from "../../npm"
|
|
||||||
import { define } from "../../plugin/internal"
|
|
||||||
import { PluginPromise } from "../../plugin/promise"
|
|
||||||
|
|
||||||
const PluginModule = Schema.Struct({
|
|
||||||
default: Schema.Union([
|
|
||||||
Schema.Struct({
|
|
||||||
id: Schema.String,
|
|
||||||
effect: Schema.declare<EffectPlugin["effect"]>(
|
|
||||||
(input): input is EffectPlugin["effect"] => typeof input === "function",
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
id: Schema.String,
|
|
||||||
setup: Schema.declare<PromisePlugin["setup"]>(
|
|
||||||
(input): input is PromisePlugin["setup"] => typeof input === "function",
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
|
|
||||||
const PluginPackage = Schema.Struct({
|
|
||||||
exports: Schema.optional(Schema.Unknown),
|
|
||||||
main: Schema.optional(Schema.String),
|
|
||||||
module: Schema.optional(Schema.String),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Plugin = define({
|
|
||||||
id: "config-plugin",
|
|
||||||
effect: Effect.fn(function* (ctx) {
|
|
||||||
const config = yield* Config.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const load = Effect.fn("ConfigExternalPlugin.load")(function* () {
|
|
||||||
const configured: { package: string; options?: Record<string, unknown> }[] = []
|
|
||||||
|
|
||||||
for (const entry of yield* config.entries()) {
|
|
||||||
if (entry.type === "document") {
|
|
||||||
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
|
||||||
for (const item of entry.info.plugins ?? []) {
|
|
||||||
const ref = typeof item === "string" ? { package: item } : item
|
|
||||||
const packageName = (() => {
|
|
||||||
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
|
|
||||||
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
|
|
||||||
return path.resolve(directory, ref.package)
|
|
||||||
}
|
|
||||||
return ref.package
|
|
||||||
})()
|
|
||||||
configured.push({ package: packageName, options: ref.options })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === "directory") {
|
|
||||||
const files = yield* fs
|
|
||||||
.glob("{plugin,plugins}/*.{ts,js}", {
|
|
||||||
cwd: entry.path,
|
|
||||||
absolute: true,
|
|
||||||
include: "file",
|
|
||||||
dot: true,
|
|
||||||
symlink: true,
|
|
||||||
})
|
|
||||||
.pipe(Effect.orElseSucceed(() => []))
|
|
||||||
const directories = yield* fs
|
|
||||||
.glob("{plugin,plugins}/*", {
|
|
||||||
cwd: entry.path,
|
|
||||||
absolute: true,
|
|
||||||
include: "all",
|
|
||||||
dot: true,
|
|
||||||
symlink: true,
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
Effect.flatMap((items) =>
|
|
||||||
Effect.filter(items, (item) => fs.isDir(item), {
|
|
||||||
concurrency: "unbounded",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Effect.orElseSucceed(() => []),
|
|
||||||
)
|
|
||||||
const packages = yield* Effect.forEach(
|
|
||||||
directories.sort(),
|
|
||||||
(directory) => resolvePackageEntrypoint(fs, directory),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
).pipe(Effect.map((items) => items.filter((item): item is string => item !== undefined)))
|
|
||||||
files.sort()
|
|
||||||
for (const file of files) configured.push({ package: file })
|
|
||||||
for (const file of packages) configured.push({ package: file })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return yield* Effect.forEach(configured, (ref) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const entrypoint = path.isAbsolute(ref.package)
|
|
||||||
? pathToFileURL(ref.package).href
|
|
||||||
: (yield* npm.add(ref.package)).entrypoint
|
|
||||||
if (!entrypoint) return
|
|
||||||
yield* Effect.log({ msg: "loading plugin", id: ref.package, entrypoint })
|
|
||||||
const mod = yield* Effect.promise(() => import(entrypoint))
|
|
||||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
|
||||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
|
||||||
return {
|
|
||||||
id: plugin.id,
|
|
||||||
effect: (host: Parameters<typeof plugin.effect>[0]) =>
|
|
||||||
plugin.effect({ ...host, options: ref.options ?? {} }),
|
|
||||||
}
|
|
||||||
}).pipe(Effect.catchCause(() => Effect.succeed(undefined))),
|
|
||||||
).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
|
|
||||||
})
|
|
||||||
for (const plugin of yield* load()) yield* ctx.plugin.add(plugin)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
|
|
||||||
const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
|
|
||||||
Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
|
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
|
||||||
)
|
|
||||||
const exported = typeof pkg?.exports === "string" ? pkg.exports : undefined
|
|
||||||
const entries = [exported, pkg?.module, pkg?.main, "index.ts", "index.js"]
|
|
||||||
|
|
||||||
return yield* Effect.forEach(entries, (entry) => {
|
|
||||||
if (!entry) return Effect.succeed(undefined)
|
|
||||||
const file = path.resolve(directory, entry)
|
|
||||||
return fs.isFile(file).pipe(Effect.map((exists) => (exists ? file : undefined)))
|
|
||||||
}).pipe(Effect.map((items) => items.find((item): item is string => item !== undefined)))
|
|
||||||
})
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "config-provider",
|
id: "opencode.config.provider",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const loaded = { entries: yield* config.entries() }
|
const loaded = { entries: yield* config.entries() }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as ConfigReferencePlugin from "./reference"
|
export * as ConfigReferencePlugin from "./reference"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
|
|
@ -11,7 +11,7 @@ import { Global } from "../../global"
|
||||||
import { Location } from "../../location"
|
import { Location } from "../../location"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "core/config-reference",
|
id: "opencode.config.reference",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as ConfigSkillPlugin from "./skill"
|
export * as ConfigSkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
|
|
@ -10,7 +10,7 @@ import { Global } from "../../global"
|
||||||
import { Location } from "../../location"
|
import { Location } from "../../location"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "config-skill",
|
id: "opencode.config.skill",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
|
|
|
||||||
|
|
@ -583,12 +583,32 @@ export const layerWith = (options?: LayerOptions) =>
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const local = <A extends Payload>(stream: Stream.Stream<A>) =>
|
||||||
|
Stream.unwrap(
|
||||||
|
Effect.serviceOption(Location.Service).pipe(
|
||||||
|
Effect.map((location) =>
|
||||||
|
Option.match(location, {
|
||||||
|
onNone: () => stream,
|
||||||
|
onSome: (location) =>
|
||||||
|
stream.pipe(
|
||||||
|
Stream.filter(
|
||||||
|
(event) =>
|
||||||
|
!event.location ||
|
||||||
|
(event.location.directory === location.directory &&
|
||||||
|
event.location.workspaceID === location.workspaceID),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
|
const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
|
||||||
Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
|
local(Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))).pipe(
|
||||||
Stream.map((event) => event as Payload<D>),
|
Stream.map((event) => event as Payload<D>),
|
||||||
)
|
)
|
||||||
|
|
||||||
const streamLive = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.live)
|
const streamLive = (): Stream.Stream<Payload> => local(Stream.fromPubSub(pubsub.live))
|
||||||
|
|
||||||
const readAfter = (
|
const readAfter = (
|
||||||
aggregateID: string,
|
aggregateID: string,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,16 @@ import { Catalog } from "./catalog"
|
||||||
import { CommandV2 } from "./command"
|
import { CommandV2 } from "./command"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { Node } from "./effect/app-node"
|
import { makeLocationNode, Node } from "./effect/app-node"
|
||||||
|
import { httpClient } from "./effect/app-node-platform"
|
||||||
|
import { EventV2 } from "./event"
|
||||||
import { FileMutation } from "./file-mutation"
|
import { FileMutation } from "./file-mutation"
|
||||||
import { FileSystem } from "./filesystem"
|
import { FileSystem } from "./filesystem"
|
||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
|
import { FSUtil } from "./fs-util"
|
||||||
import { Generate } from "./generate"
|
import { Generate } from "./generate"
|
||||||
import { Form } from "./form"
|
import { Form } from "./form"
|
||||||
|
import { Global } from "./global"
|
||||||
import { LocationWatcher } from "./filesystem/location-watcher"
|
import { LocationWatcher } from "./filesystem/location-watcher"
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
|
|
@ -18,15 +22,20 @@ import { Location } from "./location"
|
||||||
import { LocationMutation } from "./location-mutation"
|
import { LocationMutation } from "./location-mutation"
|
||||||
import { LocationServiceMap } from "./location-service-map"
|
import { LocationServiceMap } from "./location-service-map"
|
||||||
import { MCP } from "./mcp/index"
|
import { MCP } from "./mcp/index"
|
||||||
|
import { ModelsDev } from "./models-dev"
|
||||||
|
import { Npm } from "./npm"
|
||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "./permission"
|
||||||
import { PluginV2 } from "./plugin"
|
import { PluginV2 } from "./plugin"
|
||||||
import { PluginInternal } from "./plugin/internal"
|
import { PluginRuntime } from "./plugin/runtime"
|
||||||
|
import { SdkPlugins } from "./plugin/sdk"
|
||||||
|
import { PluginSupervisor } from "./plugin/supervisor"
|
||||||
import { ProjectCopy } from "./project/copy"
|
import { ProjectCopy } from "./project/copy"
|
||||||
import { Pty } from "./pty"
|
import { Pty } from "./pty"
|
||||||
import { QuestionV2 } from "./question"
|
import { QuestionV2 } from "./question"
|
||||||
import { Shell } from "./shell"
|
import { Shell } from "./shell"
|
||||||
import { Reference } from "./reference"
|
import { Reference } from "./reference"
|
||||||
import { ReferenceGuidance } from "./reference/guidance"
|
import { ReferenceGuidance } from "./reference/guidance"
|
||||||
|
import { Ripgrep } from "./ripgrep"
|
||||||
import { SessionRunnerLLM } from "./session/runner/llm"
|
import { SessionRunnerLLM } from "./session/runner/llm"
|
||||||
import { SessionRunnerModel } from "./session/runner/model"
|
import { SessionRunnerModel } from "./session/runner/model"
|
||||||
import { SessionCompaction } from "./session/compaction"
|
import { SessionCompaction } from "./session/compaction"
|
||||||
|
|
@ -42,11 +51,49 @@ import { SessionInstructions } from "./session/instructions"
|
||||||
import { McpTool } from "./tool/mcp"
|
import { McpTool } from "./tool/mcp"
|
||||||
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
||||||
import { ToolRegistry } from "./tool/registry"
|
import { ToolRegistry } from "./tool/registry"
|
||||||
|
import { WebSearchTool } from "./tool/websearch"
|
||||||
import { ToolOutputStore } from "./tool-output-store"
|
import { ToolOutputStore } from "./tool-output-store"
|
||||||
import { Vcs } from "./vcs"
|
import { Vcs } from "./vcs"
|
||||||
|
|
||||||
export { LocationServiceMap } from "./location-service-map"
|
export { LocationServiceMap } from "./location-service-map"
|
||||||
|
|
||||||
|
const pluginSupervisorNode = makeLocationNode({
|
||||||
|
service: PluginSupervisor.Service,
|
||||||
|
layer: PluginSupervisor.layer,
|
||||||
|
deps: [
|
||||||
|
PluginV2.node,
|
||||||
|
SdkPlugins.node,
|
||||||
|
AgentV2.node,
|
||||||
|
Catalog.node,
|
||||||
|
CommandV2.node,
|
||||||
|
Config.node,
|
||||||
|
EventV2.node,
|
||||||
|
FileMutation.node,
|
||||||
|
FileSystem.node,
|
||||||
|
FSUtil.node,
|
||||||
|
Global.node,
|
||||||
|
httpClient,
|
||||||
|
Image.node,
|
||||||
|
Integration.node,
|
||||||
|
Location.node,
|
||||||
|
LocationMutation.node,
|
||||||
|
ModelsDev.node,
|
||||||
|
Npm.node,
|
||||||
|
PermissionV2.node,
|
||||||
|
PluginRuntime.node,
|
||||||
|
QuestionV2.node,
|
||||||
|
ReadToolFileSystem.node,
|
||||||
|
Reference.node,
|
||||||
|
Ripgrep.node,
|
||||||
|
SessionInstructions.node,
|
||||||
|
SessionTodo.node,
|
||||||
|
Shell.node,
|
||||||
|
SkillV2.node,
|
||||||
|
ToolRegistry.toolsNode,
|
||||||
|
WebSearchTool.configNode,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const locationServiceNodes = [
|
const locationServiceNodes = [
|
||||||
Location.node,
|
Location.node,
|
||||||
Config.node,
|
Config.node,
|
||||||
|
|
@ -57,12 +104,11 @@ const locationServiceNodes = [
|
||||||
Catalog.node,
|
Catalog.node,
|
||||||
AISDK.node,
|
AISDK.node,
|
||||||
PluginV2.node,
|
PluginV2.node,
|
||||||
PluginInternal.node,
|
pluginSupervisorNode,
|
||||||
ProjectCopy.node,
|
ProjectCopy.node,
|
||||||
ProjectCopy.refreshNode,
|
ProjectCopy.refreshNode,
|
||||||
FileSystemSearch.node,
|
FileSystemSearch.node,
|
||||||
FileSystem.node,
|
FileSystem.node,
|
||||||
LocationWatcher.node,
|
|
||||||
Pty.node,
|
Pty.node,
|
||||||
Shell.node,
|
Shell.node,
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
|
|
@ -92,6 +138,8 @@ const locationServiceNodes = [
|
||||||
Snapshot.node,
|
Snapshot.node,
|
||||||
SessionRunnerLLM.node,
|
SessionRunnerLLM.node,
|
||||||
Vcs.node,
|
Vcs.node,
|
||||||
|
// Start repository watches only after boot-critical filesystem and Git work.
|
||||||
|
LocationWatcher.node,
|
||||||
] as const satisfies readonly Node.LocationNode<unknown, unknown>[]
|
] as const satisfies readonly Node.LocationNode<unknown, unknown>[]
|
||||||
|
|
||||||
export const locationServices = LayerNode.group<typeof locationServiceNodes>(locationServiceNodes)
|
export const locationServices = LayerNode.group<typeof locationServiceNodes>(locationServiceNodes)
|
||||||
|
|
@ -106,6 +154,7 @@ export function buildLocationServiceMap(
|
||||||
LocationServiceMap.Service,
|
LocationServiceMap.Service,
|
||||||
LayerMap.make(
|
LayerMap.make(
|
||||||
(ref: Location.Ref) => {
|
(ref: Location.Ref) => {
|
||||||
|
const startedAt = performance.now()
|
||||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||||
// Apply replacements during hoist, not afterward: replacements can
|
// Apply replacements during hoist, not afterward: replacements can
|
||||||
// introduce new tagged dependencies (Location.boundNode depends on
|
// introduce new tagged dependencies (Location.boundNode depends on
|
||||||
|
|
@ -116,9 +165,10 @@ export function buildLocationServiceMap(
|
||||||
return LayerNode.compile(location.node).pipe(
|
return LayerNode.compile(location.node).pipe(
|
||||||
Layer.fresh,
|
Layer.fresh,
|
||||||
Layer.tap(() =>
|
Layer.tap(() =>
|
||||||
Effect.logInfo("booting location services", {
|
Effect.logInfo("location services booted", {
|
||||||
directory: ref.directory,
|
directory: ref.directory,
|
||||||
workspaceID: ref.workspaceID,
|
workspaceID: ref.workspaceID,
|
||||||
|
durationMs: Math.round(performance.now() - startedAt),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Layer.provide(LayerNode.compile(location.hoisted)),
|
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
export * as PluginV2 from "./plugin"
|
export * as PluginV2 from "./plugin"
|
||||||
|
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
|
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
|
||||||
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/v2/effect"
|
|
||||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { AISDK } from "./aisdk"
|
import { AISDK } from "./aisdk"
|
||||||
|
|
@ -10,7 +9,6 @@ import { Catalog } from "./catalog"
|
||||||
import { CommandV2 } from "./command"
|
import { CommandV2 } from "./command"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { PluginHost } from "./plugin/host"
|
import { PluginHost } from "./plugin/host"
|
||||||
import { PluginRuntime } from "./plugin/runtime"
|
import { PluginRuntime } from "./plugin/runtime"
|
||||||
|
|
@ -27,9 +25,7 @@ export type Info = Plugin.Info
|
||||||
export const Event = Plugin.Event
|
export const Event = Plugin.Event
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly add: (id: ID, effect: PluginDefinition["effect"]) => Effect.Effect<void>
|
readonly activate: (plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[]) => Effect.Effect<void>
|
||||||
readonly remove: (id: ID) => Effect.Effect<void>
|
|
||||||
readonly wait: (id: ID) => Effect.Effect<void>
|
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,110 +35,73 @@ const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
const active = new Map<ID, Scope.Closeable>()
|
const active = new Map<ID, Scope.Closeable>()
|
||||||
const loading = new Set<ID>()
|
const lock = Semaphore.makeUnsafe(1)
|
||||||
const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
|
let generation: readonly ID[] | undefined = []
|
||||||
const failures = new Map<ID, Exit.Exit<void, never>>()
|
let host: Parameters<import("@opencode-ai/plugin/v2/effect").Plugin["effect"]>[0]
|
||||||
let host: Parameters<PluginDefinition["effect"]>[0]
|
|
||||||
|
|
||||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
|
const activate = Effect.fn("Plugin.activate")(function* (
|
||||||
if (loading.has(id)) return yield* Effect.die(new Error(`Plugin load cycle detected for ${id}`))
|
plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[],
|
||||||
|
) {
|
||||||
|
const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) }))
|
||||||
|
const ids = new Set<ID>()
|
||||||
|
for (const definition of definitions) {
|
||||||
|
if (ids.has(definition.id)) return yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`))
|
||||||
|
ids.add(definition.id)
|
||||||
|
}
|
||||||
|
|
||||||
yield* locks.withLock(id)(
|
yield* lock.withPermit(
|
||||||
Effect.sync(() => {
|
Effect.gen(function* () {
|
||||||
loading.add(id)
|
if (
|
||||||
failures.delete(id)
|
generation !== undefined &&
|
||||||
}).pipe(
|
generation.length === definitions.length &&
|
||||||
Effect.andThen(
|
generation.every((id, index) => id === definitions[index]?.id)
|
||||||
State.batch(
|
) {
|
||||||
Effect.gen(function* () {
|
return
|
||||||
const existing = active.get(id)
|
}
|
||||||
active.delete(id)
|
generation = undefined
|
||||||
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
|
const exit = yield* State.batch(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const scopes = Array.from(active.values()).toReversed()
|
||||||
|
active.clear()
|
||||||
|
const inherit = yield* State.inherit()
|
||||||
|
yield* Effect.forEach(scopes, (scope) => Scope.close(scope, Exit.void).pipe(Effect.ignore), {
|
||||||
|
discard: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const definition of definitions) {
|
||||||
const child = yield* Scope.fork(scope)
|
const child = yield* Scope.fork(scope)
|
||||||
yield* effect(host).pipe(
|
const loaded = yield* Effect.suspend(() => definition.effect(host)).pipe(
|
||||||
Scope.provide(child),
|
inherit,
|
||||||
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
|
Effect.updateContext((_context: Context.Context<never>) => Context.make(Scope.Scope, child)),
|
||||||
|
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": definition.id } }),
|
||||||
|
Effect.andThen(events.publish(Event.Added, { id: definition.id })),
|
||||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||||
|
Effect.exit,
|
||||||
)
|
)
|
||||||
yield* events.publish(Event.Added, { id })
|
if (Exit.isFailure(loaded)) return loaded
|
||||||
active.set(id, child)
|
active.set(definition.id, child)
|
||||||
yield* Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.succeed(waiter, undefined), {
|
}
|
||||||
discard: true,
|
return Exit.void
|
||||||
})
|
|
||||||
waiters.delete(id)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Effect.onExit((exit) => {
|
|
||||||
if (Exit.isSuccess(exit)) return Effect.void
|
|
||||||
failures.set(id, exit)
|
|
||||||
return Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.done(waiter, exit), {
|
|
||||||
discard: true,
|
|
||||||
}).pipe(Effect.ensuring(Effect.sync(() => waiters.delete(id))))
|
|
||||||
}),
|
|
||||||
Effect.ensuring(Effect.sync(() => loading.delete(id))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
|
||||||
if (loading.has(id)) return yield* Effect.die(new Error(`Cannot remove plugin ${id} while it is loading`))
|
|
||||||
|
|
||||||
yield* locks.withLock(id)(
|
|
||||||
State.batch(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const current = active.get(id)
|
|
||||||
active.delete(id)
|
|
||||||
failures.delete(id)
|
|
||||||
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const wait = Effect.fn("Plugin.wait")(function* (id: ID) {
|
|
||||||
const waiter = yield* Deferred.make<void>()
|
|
||||||
const pending = yield* locks.withLock(id)(
|
|
||||||
Effect.sync(() => {
|
|
||||||
if (active.has(id)) return false
|
|
||||||
const failure = failures.get(id)
|
|
||||||
if (failure) return failure
|
|
||||||
const current = waiters.get(id) ?? new Set()
|
|
||||||
current.add(waiter)
|
|
||||||
waiters.set(id, current)
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (!pending) return
|
|
||||||
if (typeof pending !== "boolean") return yield* pending
|
|
||||||
yield* Deferred.await(waiter).pipe(
|
|
||||||
Effect.ensuring(
|
|
||||||
locks.withLock(id)(
|
|
||||||
Effect.sync(() => {
|
|
||||||
const current = waiters.get(id)
|
|
||||||
current?.delete(waiter)
|
|
||||||
if (current?.size === 0) waiters.delete(id)
|
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
),
|
if (Exit.isFailure(exit)) return yield* exit
|
||||||
|
generation = definitions.map((definition) => definition.id)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* Effect.addFinalizer((exit) =>
|
yield* Effect.addFinalizer((exit) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
active.clear()
|
active.clear()
|
||||||
|
generation = []
|
||||||
yield* State.batch(Scope.close(scope, exit))
|
yield* State.batch(Scope.close(scope, exit))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const service = Service.of({
|
const service = Service.of({
|
||||||
add,
|
activate,
|
||||||
remove,
|
|
||||||
wait,
|
|
||||||
list: Effect.fn("Plugin.list")(function* () {
|
list: Effect.fn("Plugin.list")(function* () {
|
||||||
return Array.from(active.keys()).map((id) => ({ id }))
|
return Array.from(active.keys()).map((id) => ({ id }))
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export * as AgentPlugin from "./agent"
|
export * as AgentPlugin from "./agent"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { define } from "./internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
|
@ -100,7 +100,7 @@ Rules:
|
||||||
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
|
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "agent",
|
id: "opencode.agent",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const worktree = location.directory
|
const worktree = location.directory
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
export * as CommandPlugin from "./command"
|
export * as CommandPlugin from "./command"
|
||||||
|
|
||||||
import { define } from "./internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
||||||
import PROMPT_REVIEW from "./command/review.txt"
|
import PROMPT_REVIEW from "./command/review.txt"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "command",
|
id: "opencode.command",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
yield* ctx.command.transform((draft) => {
|
yield* ctx.command.transform((draft) => {
|
||||||
|
|
|
||||||
|
|
@ -273,8 +273,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
},
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
list: () => response(plugin.list()),
|
list: () => response(plugin.list()),
|
||||||
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
|
|
||||||
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
|
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
list: () => response(reference.list()),
|
list: () => response(reference.list()),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
export * as PluginInternal from "./internal"
|
export * as PluginInternal from "./internal"
|
||||||
|
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { httpClient } from "../effect/app-node-platform"
|
import { Context, Effect, Scope } from "effect"
|
||||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
import { HttpClient } from "effect/unstable/http"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
import { CommandV2 } from "../command"
|
import { CommandV2 } from "../command"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
||||||
import { ConfigCommandPlugin } from "../config/plugin/command"
|
import { ConfigCommandPlugin } from "../config/plugin/command"
|
||||||
import { ConfigExternalPlugin } from "../config/plugin/external"
|
|
||||||
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||||
|
|
@ -25,8 +23,6 @@ import { Location } from "../location"
|
||||||
import { LocationMutation } from "../location-mutation"
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
import { Npm } from "../npm"
|
import { Npm } from "../npm"
|
||||||
import { PluginV2 } from "../plugin"
|
|
||||||
import { PluginRuntime } from "../plugin/runtime"
|
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { QuestionV2 } from "../question"
|
import { QuestionV2 } from "../question"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
|
|
@ -35,177 +31,137 @@ import { SessionInstructions } from "../session/instructions"
|
||||||
import { SessionTodo } from "../session/todo"
|
import { SessionTodo } from "../session/todo"
|
||||||
import { Shell } from "../shell"
|
import { Shell } from "../shell"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { State } from "../state"
|
|
||||||
import { ToolRegistry } from "../tool/registry"
|
|
||||||
import { Tools } from "../tool/tools"
|
|
||||||
import { HttpClient } from "effect/unstable/http"
|
|
||||||
import { AgentPlugin } from "./agent"
|
|
||||||
import { CommandPlugin } from "./command"
|
|
||||||
import { ModelsDevPlugin } from "./models-dev"
|
|
||||||
import { ProviderPlugins } from "./provider"
|
|
||||||
import { SdkPlugins } from "./sdk"
|
|
||||||
import { SkillPlugin } from "./skill"
|
|
||||||
import { VariantPlugin } from "./variant"
|
|
||||||
import { ApplyPatchTool } from "../tool/apply-patch"
|
import { ApplyPatchTool } from "../tool/apply-patch"
|
||||||
import { EditTool } from "../tool/edit"
|
import { EditTool } from "../tool/edit"
|
||||||
import { GlobTool } from "../tool/glob"
|
import { GlobTool } from "../tool/glob"
|
||||||
import { GrepTool } from "../tool/grep"
|
import { GrepTool } from "../tool/grep"
|
||||||
import { QuestionTool } from "../tool/question"
|
import { QuestionTool } from "../tool/question"
|
||||||
import { ReadTool } from "../tool/read"
|
|
||||||
import { ReadToolFileSystem } from "../tool/read-filesystem"
|
import { ReadToolFileSystem } from "../tool/read-filesystem"
|
||||||
|
import { ReadTool } from "../tool/read"
|
||||||
import { ShellTool } from "../tool/shell"
|
import { ShellTool } from "../tool/shell"
|
||||||
import { SkillTool } from "../tool/skill"
|
import { SkillTool } from "../tool/skill"
|
||||||
import { SubagentTool } from "../tool/subagent"
|
import { SubagentTool } from "../tool/subagent"
|
||||||
import { TodoWriteTool } from "../tool/todowrite"
|
import { TodoWriteTool } from "../tool/todowrite"
|
||||||
|
import { Tools } from "../tool/tools"
|
||||||
import { WebFetchTool } from "../tool/webfetch"
|
import { WebFetchTool } from "../tool/webfetch"
|
||||||
import { WebSearchTool } from "../tool/websearch"
|
import { WebSearchTool } from "../tool/websearch"
|
||||||
import { WriteTool } from "../tool/write"
|
import { WriteTool } from "../tool/write"
|
||||||
|
import { AgentPlugin } from "./agent"
|
||||||
|
import { CommandPlugin } from "./command"
|
||||||
|
import { ModelsDevPlugin } from "./models-dev"
|
||||||
|
import { ProviderPlugins } from "./provider"
|
||||||
|
import { PluginRuntime } from "./runtime"
|
||||||
|
import { SkillPlugin } from "./skill"
|
||||||
|
import { VariantPlugin } from "./variant"
|
||||||
|
|
||||||
export type Requirements =
|
const services = Effect.fn("PluginInternal.services")(function* () {
|
||||||
| AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
| Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
| CommandV2.Service
|
const command = yield* CommandV2.Service
|
||||||
| Config.Service
|
const config = yield* Config.Service
|
||||||
| EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
| FileMutation.Service
|
const mutation = yield* FileMutation.Service
|
||||||
| FileSystem.Service
|
const filesystem = yield* FileSystem.Service
|
||||||
| FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
| Global.Service
|
const global = yield* Global.Service
|
||||||
| HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
| Image.Service
|
const image = yield* Image.Service
|
||||||
| Integration.Service
|
const integration = yield* Integration.Service
|
||||||
| Location.Service
|
const location = yield* Location.Service
|
||||||
| LocationMutation.Service
|
const locationMutation = yield* LocationMutation.Service
|
||||||
| ModelsDev.Service
|
const models = yield* ModelsDev.Service
|
||||||
| Npm.Service
|
const npm = yield* Npm.Service
|
||||||
| PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
| PluginRuntime.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
| QuestionV2.Service
|
const question = yield* QuestionV2.Service
|
||||||
| ReadToolFileSystem.Service
|
const read = yield* ReadToolFileSystem.Service
|
||||||
| Reference.Service
|
const reference = yield* Reference.Service
|
||||||
| Ripgrep.Service
|
const ripgrep = yield* Ripgrep.Service
|
||||||
| SessionInstructions.Service
|
const instructions = yield* SessionInstructions.Service
|
||||||
| SessionTodo.Service
|
const todo = yield* SessionTodo.Service
|
||||||
| Shell.Service
|
const shell = yield* Shell.Service
|
||||||
| SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
| Tools.Service
|
const tools = yield* Tools.Service
|
||||||
| WebSearchTool.ConfigService
|
const websearch = yield* WebSearchTool.ConfigService
|
||||||
|
return Context.mergeAll(
|
||||||
export interface Plugin<R = never> {
|
Context.make(AgentV2.Service, agent),
|
||||||
readonly id: string
|
Context.make(Catalog.Service, catalog),
|
||||||
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
|
Context.make(CommandV2.Service, command),
|
||||||
}
|
Context.make(Config.Service, config),
|
||||||
|
Context.make(EventV2.Service, events),
|
||||||
export function define<R>(plugin: Plugin<R>) {
|
Context.make(FileMutation.Service, mutation),
|
||||||
return plugin
|
Context.make(FileSystem.Service, filesystem),
|
||||||
}
|
Context.make(FSUtil.Service, fs),
|
||||||
|
Context.make(Global.Service, global),
|
||||||
const layer = Layer.effectDiscard(
|
Context.make(HttpClient.HttpClient, http),
|
||||||
Effect.gen(function* () {
|
Context.make(Image.Service, image),
|
||||||
const plugin = yield* PluginV2.Service
|
Context.make(Integration.Service, integration),
|
||||||
const sdkPlugins = yield* SdkPlugins.Service
|
Context.make(Location.Service, location),
|
||||||
const services = Context.mergeAll(
|
Context.make(LocationMutation.Service, locationMutation),
|
||||||
Context.make(Catalog.Service, yield* Catalog.Service),
|
Context.make(ModelsDev.Service, models),
|
||||||
Context.make(CommandV2.Service, yield* CommandV2.Service),
|
Context.make(Npm.Service, npm),
|
||||||
Context.make(Integration.Service, yield* Integration.Service),
|
Context.make(PermissionV2.Service, permission),
|
||||||
Context.make(AgentV2.Service, yield* AgentV2.Service),
|
Context.make(PluginRuntime.Service, runtime),
|
||||||
Context.make(Config.Service, yield* Config.Service),
|
Context.make(QuestionV2.Service, question),
|
||||||
Context.make(Location.Service, yield* Location.Service),
|
Context.make(ReadToolFileSystem.Service, read),
|
||||||
Context.make(ModelsDev.Service, yield* ModelsDev.Service),
|
Context.make(Reference.Service, reference),
|
||||||
Context.make(Npm.Service, yield* Npm.Service),
|
Context.make(Ripgrep.Service, ripgrep),
|
||||||
Context.make(EventV2.Service, yield* EventV2.Service),
|
Context.make(SessionInstructions.Service, instructions),
|
||||||
Context.make(FSUtil.Service, yield* FSUtil.Service),
|
Context.make(SessionTodo.Service, todo),
|
||||||
Context.make(FileSystem.Service, yield* FileSystem.Service),
|
Context.make(Shell.Service, shell),
|
||||||
Context.make(Global.Service, yield* Global.Service),
|
Context.make(SkillV2.Service, skill),
|
||||||
Context.make(HttpClient.HttpClient, yield* HttpClient.HttpClient),
|
Context.make(Tools.Service, tools),
|
||||||
Context.make(LocationMutation.Service, yield* LocationMutation.Service),
|
Context.make(WebSearchTool.ConfigService, websearch),
|
||||||
Context.make(FileMutation.Service, yield* FileMutation.Service),
|
)
|
||||||
Context.make(Image.Service, yield* Image.Service),
|
})
|
||||||
Context.make(PermissionV2.Service, yield* PermissionV2.Service),
|
|
||||||
Context.make(QuestionV2.Service, yield* QuestionV2.Service),
|
type ContextServices<A> = A extends Context.Context<infer R> ? R : never
|
||||||
Context.make(ReadToolFileSystem.Service, yield* ReadToolFileSystem.Service),
|
|
||||||
Context.make(SessionInstructions.Service, yield* SessionInstructions.Service),
|
export type Requirements = ContextServices<Effect.Success<ReturnType<typeof services>>>
|
||||||
Context.make(SessionTodo.Service, yield* SessionTodo.Service),
|
|
||||||
Context.make(SkillV2.Service, yield* SkillV2.Service),
|
export type InternalPlugin = Plugin<Requirements | Scope.Scope>
|
||||||
Context.make(Reference.Service, yield* Reference.Service),
|
|
||||||
Context.make(Ripgrep.Service, yield* Ripgrep.Service),
|
const pre = [
|
||||||
Context.make(Shell.Service, yield* Shell.Service),
|
AgentPlugin.Plugin,
|
||||||
Context.make(Tools.Service, yield* Tools.Service),
|
CommandPlugin.Plugin,
|
||||||
Context.make(PluginRuntime.Service, yield* PluginRuntime.Service),
|
SkillPlugin.Plugin,
|
||||||
Context.make(WebSearchTool.ConfigService, yield* WebSearchTool.ConfigService),
|
ModelsDevPlugin,
|
||||||
)
|
...ProviderPlugins,
|
||||||
const add = (input: Plugin<Requirements | Scope.Scope>) =>
|
ApplyPatchTool.Plugin,
|
||||||
plugin.add(PluginV2.ID.make(input.id), (context: PluginContext) =>
|
EditTool.Plugin,
|
||||||
input.effect(context).pipe(Effect.provide(services)),
|
GlobTool.Plugin,
|
||||||
)
|
GrepTool.Plugin,
|
||||||
|
QuestionTool.Plugin,
|
||||||
yield* State.batch(
|
ReadTool.Plugin,
|
||||||
Effect.gen(function* () {
|
ShellTool.Plugin,
|
||||||
yield* add(ConfigReferencePlugin.Plugin)
|
SkillTool.Plugin,
|
||||||
yield* add(AgentPlugin.Plugin)
|
SubagentTool.Plugin,
|
||||||
yield* add(CommandPlugin.Plugin)
|
TodoWriteTool.Plugin,
|
||||||
yield* add(SkillPlugin.Plugin)
|
WebFetchTool.Plugin,
|
||||||
yield* add(ModelsDevPlugin)
|
WebSearchTool.Plugin,
|
||||||
yield* add(ConfigExternalPlugin.Plugin)
|
WriteTool.Plugin,
|
||||||
yield* add(ApplyPatchTool.Plugin)
|
] as const satisfies readonly InternalPlugin[]
|
||||||
yield* add(EditTool.Plugin)
|
|
||||||
yield* add(GlobTool.Plugin)
|
const post = [
|
||||||
yield* add(GrepTool.Plugin)
|
ConfigReferencePlugin.Plugin,
|
||||||
yield* add(QuestionTool.Plugin)
|
ConfigAgentPlugin.Plugin,
|
||||||
yield* add(ReadTool.Plugin)
|
ConfigCommandPlugin.Plugin,
|
||||||
yield* add(ShellTool.Plugin)
|
ConfigSkillPlugin.Plugin,
|
||||||
yield* add(SkillTool.Plugin)
|
ConfigProviderPlugin.Plugin,
|
||||||
yield* add(SubagentTool.Plugin)
|
VariantPlugin.Plugin,
|
||||||
yield* add(TodoWriteTool.Plugin)
|
] as const satisfies readonly InternalPlugin[]
|
||||||
yield* add(WebFetchTool.Plugin)
|
|
||||||
yield* add(WebSearchTool.Plugin)
|
export const list = Effect.fn("PluginInternal.list")(function* () {
|
||||||
yield* add(WriteTool.Plugin)
|
const context = yield* services()
|
||||||
yield* add(ConfigAgentPlugin.Plugin)
|
const resolve = (plugins: readonly InternalPlugin[]) =>
|
||||||
yield* add(ConfigCommandPlugin.Plugin)
|
plugins.map(
|
||||||
yield* add(ConfigSkillPlugin.Plugin)
|
(plugin): Plugin => ({
|
||||||
for (const item of ProviderPlugins) yield* add(item)
|
id: plugin.id,
|
||||||
yield* add(ConfigProviderPlugin.Plugin)
|
effect: (host) => plugin.effect(host).pipe(Effect.provide(context)),
|
||||||
yield* add(VariantPlugin.Plugin)
|
}),
|
||||||
// Embedder-contributed plugins are added last so they layer over config.
|
)
|
||||||
for (const plugin of sdkPlugins.all()) yield* add(plugin)
|
return {
|
||||||
}),
|
pre: resolve(pre),
|
||||||
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
post: resolve(post),
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeLocationNode({
|
|
||||||
name: "plugin-internal",
|
|
||||||
layer,
|
|
||||||
deps: [
|
|
||||||
Catalog.node,
|
|
||||||
CommandV2.node,
|
|
||||||
PluginV2.node,
|
|
||||||
Integration.node,
|
|
||||||
AgentV2.node,
|
|
||||||
Config.node,
|
|
||||||
Location.node,
|
|
||||||
LocationMutation.node,
|
|
||||||
FileMutation.node,
|
|
||||||
Image.node,
|
|
||||||
ModelsDev.node,
|
|
||||||
Npm.node,
|
|
||||||
EventV2.node,
|
|
||||||
FSUtil.node,
|
|
||||||
FileSystem.node,
|
|
||||||
Global.node,
|
|
||||||
httpClient,
|
|
||||||
PermissionV2.node,
|
|
||||||
QuestionV2.node,
|
|
||||||
ReadToolFileSystem.node,
|
|
||||||
SessionInstructions.node,
|
|
||||||
SessionTodo.node,
|
|
||||||
SkillV2.node,
|
|
||||||
Reference.node,
|
|
||||||
Ripgrep.node,
|
|
||||||
Shell.node,
|
|
||||||
ToolRegistry.toolsNode,
|
|
||||||
PluginRuntime.node,
|
|
||||||
SdkPlugins.node,
|
|
||||||
WebSearchTool.configNode,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { define } from "./internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
|
|
@ -197,7 +197,7 @@ function applyModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelsDevPlugin = define({
|
export const ModelsDevPlugin = define({
|
||||||
id: "models-dev",
|
id: "opencode.models-dev",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const modelsDev = yield* ModelsDev.Service
|
const modelsDev = yield* ModelsDev.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ type Registration = { readonly dispose: () => Promise<void> }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||||
* loader (`PluginV2` / `PluginInternal`) can run it unchanged.
|
* loader (`PluginV2` / `PluginSupervisor`) can run it unchanged.
|
||||||
*
|
*
|
||||||
* Hook registrations created during the async `setup` attach to the plugin's
|
* Hook registrations created during the async `setup` attach to the plugin's
|
||||||
* scope, so unloading the plugin disposes them. The captured fiber context
|
* scope, so unloading the plugin disposes them. The captured fiber context
|
||||||
|
|
@ -93,11 +93,6 @@ export function fromPromise(plugin: Plugin) {
|
||||||
},
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
list: (input) => run(host.plugin.list(input)),
|
list: (input) => run(host.plugin.list(input)),
|
||||||
add: (input) => {
|
|
||||||
const child = fromPromise(input)
|
|
||||||
return run(host.plugin.add(child))
|
|
||||||
},
|
|
||||||
remove: (id) => run(host.plugin.remove(id)),
|
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
list: (input) => run(host.reference.list(input)),
|
list: (input) => run(host.reference.list(input)),
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ import { VenicePlugin } from "./provider/venice"
|
||||||
import { XAIPlugin } from "./provider/xai"
|
import { XAIPlugin } from "./provider/xai"
|
||||||
import { ZenmuxPlugin } from "./provider/zenmux"
|
import { ZenmuxPlugin } from "./provider/zenmux"
|
||||||
import type { PluginInternal } from "./internal"
|
import type { PluginInternal } from "./internal"
|
||||||
import type { Scope } from "effect"
|
|
||||||
|
|
||||||
export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
|
export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
|
||||||
AlibabaPlugin,
|
AlibabaPlugin,
|
||||||
AmazonBedrockPlugin,
|
AmazonBedrockPlugin,
|
||||||
AnthropicPlugin,
|
AnthropicPlugin,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const AlibabaPlugin = define({
|
export const AlibabaPlugin = define({
|
||||||
id: "alibaba",
|
id: "opencode.provider.alibaba",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
type MantleSDK = {
|
type MantleSDK = {
|
||||||
|
|
@ -60,7 +60,7 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmazonBedrockPlugin = define({
|
export const AmazonBedrockPlugin = define({
|
||||||
id: "amazon-bedrock",
|
id: "opencode.provider.amazon-bedrock",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const AnthropicPlugin = define({
|
export const AnthropicPlugin = define({
|
||||||
id: "anthropic",
|
id: "opencode.provider.anthropic",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||||
|
|
@ -11,7 +11,7 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AzurePlugin = define({
|
export const AzurePlugin = define({
|
||||||
id: "azure",
|
id: "opencode.provider.azure",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
@ -54,7 +54,7 @@ export const AzurePlugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AzureCognitiveServicesPlugin = define({
|
export const AzureCognitiveServicesPlugin = define({
|
||||||
id: "azure-cognitive-services",
|
id: "opencode.provider.azure-cognitive-services",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const CerebrasPlugin = define({
|
export const CerebrasPlugin = define({
|
||||||
id: "cerebras",
|
id: "opencode.provider.cerebras",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const CloudflareAIGatewayPlugin = define({
|
export const CloudflareAIGatewayPlugin = define({
|
||||||
id: "cloudflare-ai-gateway",
|
id: "opencode.provider.cloudflare-ai-gateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||||
|
|
||||||
export const CloudflareWorkersAIPlugin = define({
|
export const CloudflareWorkersAIPlugin = define({
|
||||||
id: "cloudflare-workers-ai",
|
id: "opencode.provider.cloudflare-workers-ai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
const item = evt.provider.get(providerID)
|
const item = evt.provider.get(providerID)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const CoherePlugin = define({
|
export const CoherePlugin = define({
|
||||||
id: "cohere",
|
id: "opencode.provider.cohere",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const DeepInfraPlugin = define({
|
export const DeepInfraPlugin = define({
|
||||||
id: "deepinfra",
|
id: "opencode.provider.deepinfra",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Npm } from "../../npm"
|
import { Npm } from "../../npm"
|
||||||
|
|
||||||
export const DynamicProviderPlugin = define({
|
export const DynamicProviderPlugin = define({
|
||||||
id: "dynamic-provider",
|
id: "opencode.provider.dynamic",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const GatewayPlugin = define({
|
export const GatewayPlugin = define({
|
||||||
id: "gateway",
|
id: "opencode.provider.gateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function shouldUseResponses(modelID: string) {
|
function shouldUseResponses(modelID: string) {
|
||||||
|
|
@ -12,7 +12,7 @@ function shouldUseResponses(modelID: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GithubCopilotPlugin = define({
|
export const GithubCopilotPlugin = define({
|
||||||
id: "github-copilot",
|
id: "opencode.provider.github-copilot",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const GitLabPlugin = define({
|
export const GitLabPlugin = define({
|
||||||
id: "gitlab",
|
id: "opencode.provider.gitlab",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
function resolveProject(options: Record<string, any>) {
|
function resolveProject(options: Record<string, any>) {
|
||||||
|
|
@ -55,7 +55,7 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoogleVertexPlugin = define({
|
export const GoogleVertexPlugin = define({
|
||||||
id: "google-vertex",
|
id: "opencode.provider.google-vertex",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
@ -111,7 +111,7 @@ export const GoogleVertexPlugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GoogleVertexAnthropicPlugin = define({
|
export const GoogleVertexAnthropicPlugin = define({
|
||||||
id: "google-vertex-anthropic",
|
id: "opencode.provider.google-vertex-anthropic",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const GooglePlugin = define({
|
export const GooglePlugin = define({
|
||||||
id: "google",
|
id: "opencode.provider.google",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const GroqPlugin = define({
|
export const GroqPlugin = define({
|
||||||
id: "groq",
|
id: "opencode.provider.groq",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const KiloPlugin = define({
|
export const KiloPlugin = define({
|
||||||
id: "kilo",
|
id: "opencode.provider.kilo",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
|
|
||||||
export const LLMGatewayPlugin = define({
|
export const LLMGatewayPlugin = define({
|
||||||
id: "llmgateway",
|
id: "opencode.provider.llmgateway",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
|
const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const MistralPlugin = define({
|
export const MistralPlugin = define({
|
||||||
id: "mistral",
|
id: "opencode.provider.mistral",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const NvidiaPlugin = define({
|
export const NvidiaPlugin = define({
|
||||||
id: "nvidia",
|
id: "opencode.provider.nvidia",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const OpenAICompatiblePlugin = define({
|
export const OpenAICompatiblePlugin = define({
|
||||||
id: "openai-compatible",
|
id: "opencode.provider.openai-compatible",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { createServer } from "node:http"
|
||||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
|
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
|
||||||
import type { Scope } from "effect"
|
|
||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
import { EventV2 } from "../../event"
|
import { EventV2 } from "../../event"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
|
|
@ -159,7 +158,7 @@ const headless = {
|
||||||
} satisfies IntegrationOAuthMethodRegistration
|
} satisfies IntegrationOAuthMethodRegistration
|
||||||
|
|
||||||
export const OpenAIPlugin = define({
|
export const OpenAIPlugin = define({
|
||||||
id: "openai",
|
id: "opencode.provider.openai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const loading = Semaphore.makeUnsafe(1)
|
const loading = Semaphore.makeUnsafe(1)
|
||||||
|
|
@ -225,7 +224,7 @@ export const OpenAIPlugin = define({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
} satisfies PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>)
|
} satisfies PluginInternal.InternalPlugin)
|
||||||
|
|
||||||
function headers(contentType: string) {
|
function headers(contentType: string) {
|
||||||
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }
|
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ function oauth(http: HttpClient.HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({
|
export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({
|
||||||
id: "opencode",
|
id: "opencode.provider.opencode",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const OpenRouterPlugin = define({
|
export const OpenRouterPlugin = define({
|
||||||
id: "openrouter",
|
id: "opencode.provider.openrouter",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const PerplexityPlugin = define({
|
export const PerplexityPlugin = define({
|
||||||
id: "perplexity",
|
id: "opencode.provider.perplexity",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Npm } from "../../npm"
|
import { Npm } from "../../npm"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const SapAICorePlugin = define({
|
export const SapAICorePlugin = define({
|
||||||
id: "sap-ai-core",
|
id: "opencode.provider.sap-ai-core",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
||||||
|
|
@ -65,7 +65,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnowflakeCortexPlugin = define({
|
export const SnowflakeCortexPlugin = define({
|
||||||
id: "snowflake-cortex",
|
id: "opencode.provider.snowflake-cortex",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const TogetherAIPlugin = define({
|
export const TogetherAIPlugin = define({
|
||||||
id: "togetherai",
|
id: "opencode.provider.togetherai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const VenicePlugin = define({
|
export const VenicePlugin = define({
|
||||||
id: "venice",
|
id: "opencode.provider.venice",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const VercelPlugin = define({
|
export const VercelPlugin = define({
|
||||||
id: "vercel",
|
id: "opencode.provider.vercel",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const XAIPlugin = define({
|
export const XAIPlugin = define({
|
||||||
id: "xai",
|
id: "opencode.provider.xai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "../internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const ZenmuxPlugin = define({
|
export const ZenmuxPlugin = define({
|
||||||
id: "zenmux",
|
id: "opencode.provider.zenmux",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ const defaultStore = makeStore()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||||
* so `PluginInternal` can add them on every Location boot through the ordinary
|
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
||||||
* `ctx.plugin.add` seam — the same path `ConfigExternalPlugin` uses for plugins
|
* generation path that `PluginSupervisor` uses for plugins discovered from
|
||||||
* discovered from config. A plugin registered after a Location has booted only
|
* config. A plugin registered after a Location has booted only
|
||||||
* applies to Locations booted afterward, matching config-plugin timing;
|
* applies to Locations booted afterward, matching config-plugin timing;
|
||||||
* embedders register at startup before creating Sessions.
|
* embedders register at startup before creating Sessions.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
export * as SkillPlugin from "./skill"
|
export * as SkillPlugin from "./skill"
|
||||||
|
|
||||||
import { define } from "./internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
|
|
@ -25,7 +25,7 @@ const REPORT_DESCRIPTION =
|
||||||
"Use when the user wants to report an opencode issue or bug. Collect standard diagnostics, add user-specific reproduction context, and publish the issue with GitHub CLI."
|
"Use when the user wants to report an opencode issue or bug. Collect standard diagnostics, add user-specific reproduction context, and publish the issue with GitHub CLI."
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "skill",
|
id: "opencode.skill",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const reportContent = yield* reportContentWithDiagnostics()
|
const reportContent = yield* reportContentWithDiagnostics()
|
||||||
yield* ctx.skill.transform((draft) => {
|
yield* ctx.skill.transform((draft) => {
|
||||||
|
|
|
||||||
270
packages/core/src/plugin/supervisor.ts
Normal file
270
packages/core/src/plugin/supervisor.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
export * as PluginSupervisor from "./supervisor"
|
||||||
|
|
||||||
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Event } from "@opencode-ai/schema/config"
|
||||||
|
import { Context, Effect, Fiber, Layer, Schema, Semaphore, Stream } from "effect"
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath, pathToFileURL } from "url"
|
||||||
|
import { Config } from "../config"
|
||||||
|
import { ConfigPlugin } from "../config/plugin"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { Npm } from "../npm"
|
||||||
|
import { PluginV2 } from "../plugin"
|
||||||
|
import { PluginPromise } from "../plugin/promise"
|
||||||
|
import { PluginInternal } from "./internal"
|
||||||
|
import { SdkPlugins } from "./sdk"
|
||||||
|
|
||||||
|
const PluginModule = Schema.Struct({
|
||||||
|
default: Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
|
effect: Schema.declare<Plugin["effect"]>((input): input is Plugin["effect"] => typeof input === "function"),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
|
setup: Schema.declare<Parameters<typeof PluginPromise.fromPromise>[0]["setup"]>(
|
||||||
|
(input): input is Parameters<typeof PluginPromise.fromPromise>[0]["setup"] => typeof input === "function",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PluginPackage = Schema.Struct({
|
||||||
|
exports: Schema.optional(Schema.Unknown),
|
||||||
|
main: Schema.optional(Schema.String),
|
||||||
|
module: Schema.optional(Schema.String),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Operation =
|
||||||
|
| {
|
||||||
|
readonly type: "add"
|
||||||
|
readonly target: string
|
||||||
|
readonly options: Record<string, unknown>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "remove"
|
||||||
|
readonly target: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Candidate =
|
||||||
|
| {
|
||||||
|
readonly type: "definition"
|
||||||
|
readonly definition: Plugin
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "package"
|
||||||
|
readonly specifier: string
|
||||||
|
readonly options: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfiguredPackage = {
|
||||||
|
readonly operation: Extract<Operation, { type: "add" }>
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(input: ConfigPlugin.Plugin): Operation {
|
||||||
|
if (typeof input !== "string") {
|
||||||
|
return { type: "add", target: input.package, options: input.options ?? {} }
|
||||||
|
}
|
||||||
|
if (!input.startsWith("-")) return { type: "add", target: input, options: {} }
|
||||||
|
if (input.length === 1) throw new Error("Plugin remove operation requires a target")
|
||||||
|
return { type: "remove", target: input.slice(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Config.Entry[]) {
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const discovered = yield* Effect.forEach(
|
||||||
|
entries.filter((entry): entry is Config.Directory => entry.type === "directory"),
|
||||||
|
(entry) => discoverDirectory(fs, entry.path),
|
||||||
|
).pipe(Effect.map((items) => items.flat()))
|
||||||
|
const configured = entries
|
||||||
|
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
|
.flatMap((entry) =>
|
||||||
|
(entry.info.plugins ?? []).map(parse).map((operation) => {
|
||||||
|
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
||||||
|
const target = operation.target.startsWith("file://")
|
||||||
|
? fileURLToPath(operation.target)
|
||||||
|
: operation.target.startsWith("./") || operation.target.startsWith("../")
|
||||||
|
? path.resolve(directory, operation.target)
|
||||||
|
: operation.target
|
||||||
|
return operation.type === "add" ? { ...operation, target } : { type: "remove" as const, target }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// Explicit config is applied last so it can remove auto-discovered packages.
|
||||||
|
return [...discovered, ...configured]
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
|
||||||
|
pre: readonly Plugin[],
|
||||||
|
post: readonly Plugin[],
|
||||||
|
operations: readonly Operation[],
|
||||||
|
) {
|
||||||
|
const plan = apply(pre, post, operations)
|
||||||
|
return yield* load(plan)
|
||||||
|
})
|
||||||
|
|
||||||
|
function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: readonly Operation[]) {
|
||||||
|
const matches = (selector: string, target: string) =>
|
||||||
|
selector === "*" || (selector.endsWith(".*") ? target.startsWith(selector.slice(0, -1)) : selector === target)
|
||||||
|
const plugins = [...pre, ...post]
|
||||||
|
const enabled = new Set(plugins.map((plugin) => plugin.id))
|
||||||
|
const packages = new Map<string, ConfiguredPackage>()
|
||||||
|
|
||||||
|
for (const operation of operations) {
|
||||||
|
if (operation.type === "remove") {
|
||||||
|
plugins.filter((plugin) => matches(operation.target, plugin.id)).forEach((plugin) => enabled.delete(plugin.id))
|
||||||
|
packages.forEach((item, target) => {
|
||||||
|
if (matches(operation.target, target)) item.enabled = false
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = plugins.filter((plugin) => matches(operation.target, plugin.id))
|
||||||
|
const selectsDefinitions =
|
||||||
|
matched.length > 0 ||
|
||||||
|
operation.target === "*" ||
|
||||||
|
operation.target.endsWith(".*") ||
|
||||||
|
operation.target.startsWith("opencode.")
|
||||||
|
if (selectsDefinitions) {
|
||||||
|
matched.forEach((plugin) => enabled.add(plugin.id))
|
||||||
|
packages.forEach((item, target) => {
|
||||||
|
if (matches(operation.target, target)) item.enabled = true
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
packages.set(operation.target, { operation, enabled: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const definitions: Candidate[] = pre.flatMap((definition) =>
|
||||||
|
enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
|
||||||
|
)
|
||||||
|
const configured: Candidate[] = Array.from(packages.values()).flatMap((item) =>
|
||||||
|
item.enabled ? [{ type: "package", specifier: item.operation.target, options: item.operation.options }] : [],
|
||||||
|
)
|
||||||
|
const posts: Candidate[] = post.flatMap((definition) =>
|
||||||
|
enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
|
||||||
|
)
|
||||||
|
return [...definitions, ...configured, ...posts]
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = Effect.fn("PluginSupervisor.load")(function* (plan: readonly Candidate[]) {
|
||||||
|
return yield* Effect.forEach(plan, (candidate) => {
|
||||||
|
if (candidate.type === "definition") return Effect.succeed(candidate.definition)
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const entrypoint = path.isAbsolute(candidate.specifier)
|
||||||
|
? pathToFileURL(candidate.specifier).href
|
||||||
|
: (yield* npm.add(candidate.specifier)).entrypoint
|
||||||
|
if (!entrypoint) return
|
||||||
|
yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint })
|
||||||
|
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||||
|
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||||
|
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||||
|
return {
|
||||||
|
id: plugin.id,
|
||||||
|
effect: (host) => plugin.effect({ ...host, options: candidate.options }),
|
||||||
|
} satisfies Plugin
|
||||||
|
}).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
|
||||||
|
}).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
|
||||||
|
})
|
||||||
|
|
||||||
|
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const files = yield* fs
|
||||||
|
.glob("{plugin,plugins}/*.{ts,js}", {
|
||||||
|
cwd: directory,
|
||||||
|
absolute: true,
|
||||||
|
include: "file",
|
||||||
|
dot: true,
|
||||||
|
symlink: true,
|
||||||
|
})
|
||||||
|
.pipe(Effect.orElseSucceed(() => []))
|
||||||
|
const directories = yield* fs
|
||||||
|
.glob("{plugin,plugins}/*", {
|
||||||
|
cwd: directory,
|
||||||
|
absolute: true,
|
||||||
|
include: "all",
|
||||||
|
dot: true,
|
||||||
|
symlink: true,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.flatMap((items) => Effect.filter(items, (item) => fs.isDir(item), { concurrency: "unbounded" })),
|
||||||
|
Effect.orElseSucceed(() => []),
|
||||||
|
)
|
||||||
|
const packages = yield* Effect.forEach(directories.sort(), (directory) => resolvePackageEntrypoint(fs, directory), {
|
||||||
|
concurrency: "unbounded",
|
||||||
|
}).pipe(Effect.map((items) => items.filter((item): item is string => item !== undefined)))
|
||||||
|
return [...files.sort(), ...packages].map((target): Operation => ({ type: "add", target, options: {} }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
|
||||||
|
const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
|
||||||
|
Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
|
||||||
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
|
)
|
||||||
|
const exported = typeof pkg?.exports === "string" ? pkg.exports : undefined
|
||||||
|
const entries = [exported, pkg?.module, pkg?.main, "index.ts", "index.js"]
|
||||||
|
|
||||||
|
return yield* Effect.forEach(entries, (entry) => {
|
||||||
|
if (!entry) return Effect.succeed(undefined)
|
||||||
|
const file = path.resolve(directory, entry)
|
||||||
|
return fs.isFile(file).pipe(Effect.map((exists) => (exists ? file : undefined)))
|
||||||
|
}).pipe(Effect.map((items) => items.find((item): item is string => item !== undefined)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly ready: Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginSupervisor") {}
|
||||||
|
|
||||||
|
const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* PluginV2.Service
|
||||||
|
const sdk = yield* SdkPlugins.Service
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const lock = Semaphore.makeUnsafe(1)
|
||||||
|
let applied: string | undefined
|
||||||
|
const reload = Effect.fn("PluginSupervisor.reload")(() =>
|
||||||
|
lock.withPermit(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
// Resolve OpenCode's internal plugins with their privileged Location services.
|
||||||
|
const internal = yield* PluginInternal.list()
|
||||||
|
// Combine internal plugins with host-contributed SDK plugins in boot order.
|
||||||
|
const pre = [...internal.pre, ...sdk.all()]
|
||||||
|
// Read the current layered config before resolving plugin directives and packages.
|
||||||
|
const entries = yield* config.entries()
|
||||||
|
// Skip duplicate watcher notifications and config edits unrelated to plugins.
|
||||||
|
const operations = yield* scan(entries)
|
||||||
|
const version = JSON.stringify(operations)
|
||||||
|
if (version === applied) return
|
||||||
|
// Apply config operations and load enabled package plugins into one ordered generation.
|
||||||
|
const plugins = yield* resolve(pre, internal.post, operations)
|
||||||
|
// Replace the active generation in one scoped, batched activation.
|
||||||
|
yield* registry.activate(plugins)
|
||||||
|
applied = version
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield* events.subscribe(Event.Updated).pipe(
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload plugins", { cause }))),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
const fiber = yield* reload().pipe(
|
||||||
|
Effect.withSpan("PluginSupervisor.boot"),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
return Service.of({ ready: Fiber.join(fiber) })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export { layer }
|
||||||
|
|
@ -2,10 +2,10 @@ export * as VariantPlugin from "./variant"
|
||||||
|
|
||||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "./internal"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "variant",
|
id: "opencode.variant",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((catalog) => {
|
yield* ctx.catalog.transform((catalog) => {
|
||||||
for (const record of catalog.provider.list()) {
|
for (const record of catalog.provider.list()) {
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,32 @@ export interface Transformable<DraftApi> {
|
||||||
readonly reload: Reload
|
readonly reload: Reload
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
|
type Batch = {
|
||||||
|
active: boolean
|
||||||
|
readonly reloads: Set<Reload>
|
||||||
|
}
|
||||||
|
|
||||||
|
const CurrentBatch = Context.Reference<Batch | undefined>("@opencode/State/CurrentBatch", {
|
||||||
defaultValue: () => undefined,
|
defaultValue: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const current = yield* CurrentBatch
|
const current = yield* CurrentBatch
|
||||||
if (current) return yield* effect
|
if (current?.active) return yield* effect
|
||||||
const reloads = new Set<Reload>()
|
const batch: Batch = { active: true, reloads: new Set() }
|
||||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
|
const exit = yield* effect.pipe(Effect.provideService(CurrentBatch, batch), Effect.exit)
|
||||||
yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
|
batch.active = false
|
||||||
return result
|
yield* Effect.forEach(batch.reloads, (reload) => reload(), { discard: true })
|
||||||
|
return yield* exit
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const inherit = Effect.fnUntraced(function* () {
|
||||||
|
const batch = yield* CurrentBatch
|
||||||
|
return <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.provideService(effect, CurrentBatch, batch)
|
||||||
|
})
|
||||||
|
|
||||||
export interface Options<State, DraftApi> {
|
export interface Options<State, DraftApi> {
|
||||||
/** Creates the base value for initial state and every scoped-transform reload. */
|
/** Creates the base value for initial state and every scoped-transform reload. */
|
||||||
readonly initial: () => State
|
readonly initial: () => State
|
||||||
|
|
@ -100,8 +111,8 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
||||||
transforms = transforms.filter((item) => item !== transform)
|
transforms = transforms.filter((item) => item !== transform)
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const batch = yield* CurrentBatch
|
const batch = yield* CurrentBatch
|
||||||
if (batch) {
|
if (batch?.active) {
|
||||||
batch.add(reload)
|
batch.reloads.add(reload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
yield* materialize()
|
yield* materialize()
|
||||||
|
|
@ -116,7 +127,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
||||||
)
|
)
|
||||||
yield* Scope.addFinalizer(scope, dispose)
|
yield* Scope.addFinalizer(scope, dispose)
|
||||||
const batch = yield* CurrentBatch
|
const batch = yield* CurrentBatch
|
||||||
if (batch) batch.add(reload)
|
if (batch?.active) batch.reloads.add(reload)
|
||||||
else yield* reload()
|
else yield* reload()
|
||||||
return { dispose }
|
return { dispose }
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ type Prepared =
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-apply-patch-tool",
|
id: "opencode.tool.apply-patch",
|
||||||
effect: Effect.fn("ApplyPatchTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("ApplyPatchTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const mutation = yield* LocationMutation.Service
|
const mutation = yield* LocationMutation.Service
|
||||||
const files = yield* FileMutation.Service
|
const files = yield* FileMutation.Service
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export const toModelOutput = (output: Output, oldString: string, newString: stri
|
||||||
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
|
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-edit-tool",
|
id: "opencode.tool.edit",
|
||||||
effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const mutation = yield* LocationMutation.Service
|
const mutation = yield* LocationMutation.Service
|
||||||
const files = yield* FileMutation.Service
|
const files = yield* FileMutation.Service
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const toModelOutput = (output: ModelOutput) => {
|
||||||
|
|
||||||
/** Glob leaf that defaults its filesystem root to the active Location. */
|
/** Glob leaf that defaults its filesystem root to the active Location. */
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-glob-tool",
|
id: "opencode.tool.glob",
|
||||||
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const ripgrep = yield* Ripgrep.Service
|
const ripgrep = yield* Ripgrep.Service
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export const toModelOutput = (output: ModelOutput) => {
|
||||||
|
|
||||||
/** Grep leaf that defaults its filesystem root to the active Location. */
|
/** Grep leaf that defaults its filesystem root to the active Location. */
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-grep-tool",
|
id: "opencode.tool.grep",
|
||||||
effect: Effect.fn("GrepTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("GrepTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const ripgrep = yield* Ripgrep.Service
|
const ripgrep = yield* Ripgrep.Service
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const toModelOutput = (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-question-tool",
|
id: "opencode.tool.question",
|
||||||
effect: Effect.fn("QuestionTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("QuestionTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const question = yield* QuestionV2.Service
|
const question = yield* QuestionV2.Service
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const Input = LocationInput
|
||||||
const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
|
const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-read-tool",
|
id: "opencode.tool.read",
|
||||||
effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const reader = yield* ReadToolFileSystem.Service
|
const reader = yield* ReadToolFileSystem.Service
|
||||||
const mutation = yield* LocationMutation.Service
|
const mutation = yield* LocationMutation.Service
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ const externalCommandDirectories = Effect.fn("ShellTool.externalCommandDirectori
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-shell-tool",
|
id: "opencode.tool.shell",
|
||||||
effect: Effect.fn("ShellTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("ShellTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const runtime = yield* PluginRuntime.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const unableToLoad = (name: string, error?: unknown) =>
|
||||||
new ToolFailure({ message: `Unable to load skill ${name}`, error })
|
new ToolFailure({ message: `Unable to load skill ${name}`, error })
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-skill-tool",
|
id: "opencode.tool.skill",
|
||||||
effect: Effect.fn("SkillTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("SkillTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const skills = yield* SkillV2.Service
|
const skills = yield* SkillV2.Service
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export const description = [
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-subagent-tool",
|
id: "opencode.tool.subagent",
|
||||||
effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const runtime = yield* PluginRuntime.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type Output = typeof Output.Type
|
||||||
export const toModelOutput = (output: Output) => JSON.stringify(output.todos, null, 2)
|
export const toModelOutput = (output: Output) => JSON.stringify(output.todos, null, 2)
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-todowrite-tool",
|
id: "opencode.tool.todowrite",
|
||||||
effect: Effect.fn("TodoWriteTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("TodoWriteTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const todos = yield* SessionTodo.Service
|
const todos = yield* SessionTodo.Service
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ const convert = (content: string, contentType: string, format: Format) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-webfetch-tool",
|
id: "opencode.tool.webfetch",
|
||||||
effect: Effect.fn("WebFetchTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("WebFetchTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ const Output = Schema.Struct({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-websearch-tool",
|
id: "opencode.tool.websearch",
|
||||||
effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
const config = yield* ConfigService
|
const config = yield* ConfigService
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const toModelOutput = (output: Output) =>
|
||||||
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
|
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-write-tool",
|
id: "opencode.tool.write",
|
||||||
effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const mutation = yield* LocationMutation.Service
|
const mutation = yield* LocationMutation.Service
|
||||||
const files = yield* FileMutation.Service
|
const files = yield* FileMutation.Service
|
||||||
|
|
|
||||||
|
|
@ -1,252 +1,225 @@
|
||||||
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Schema } from "effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { Npm } from "@opencode-ai/core/npm"
|
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Database } from "../../src/database/database"
|
||||||
|
import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "../plugin/fixture"
|
|
||||||
|
|
||||||
const it = testEffect(PluginTestLayer)
|
const it = testEffect(
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
|
||||||
|
)
|
||||||
|
|
||||||
describe("ConfigExternalPlugin", () => {
|
describe("PluginSupervisor config", () => {
|
||||||
it.live("resolves and loads a configured Promise plugin with options", () =>
|
it.live("applies selectors in order", () =>
|
||||||
|
withLocation(
|
||||||
|
{ plugins: ["-opencode.provider.*", "opencode.provider.openai"] },
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
yield* ready()
|
||||||
|
expect(
|
||||||
|
(yield* plugins.list()).map((plugin) => plugin.id).filter((id) => id.startsWith("opencode.provider.")),
|
||||||
|
).toEqual([PluginV2.ID.make("opencode.provider.openai")])
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads configured Promise plugins with options", () =>
|
||||||
|
withLocation(
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
"-*",
|
||||||
|
{
|
||||||
|
package: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||||
|
options: { description: "Loaded from config" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({
|
||||||
|
description: "Loaded from config",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads configured Effect plugins with options", () =>
|
||||||
|
withLocation(
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
"-*",
|
||||||
|
{
|
||||||
|
package: path.join(import.meta.dir, "../plugin/fixtures/config-effect-plugin.ts"),
|
||||||
|
options: { description: "Effect plugin from config" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("effect-configured"))).toMatchObject({
|
||||||
|
description: "Effect plugin from config",
|
||||||
|
mode: "subagent",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("ignores invalid packages and continues loading", () =>
|
||||||
|
withLocation(
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
"-*",
|
||||||
|
path.join(import.meta.dir, "../plugin/fixtures/missing-plugin.ts"),
|
||||||
|
path.join(import.meta.dir, "../plugin/fixtures/invalid-plugin.ts"),
|
||||||
|
{
|
||||||
|
package: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||||
|
options: { description: "Loaded after invalid plugins" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({
|
||||||
|
description: "Loaded after invalid plugins",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads auto-discovered plugin files and packages", () =>
|
||||||
|
withLocation(
|
||||||
|
undefined,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({
|
||||||
|
description: "Loaded from plugin directory",
|
||||||
|
})
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("folder"))).toMatchObject({
|
||||||
|
description: "Loaded from plugin folder",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("applies explicit removals after auto-discovery", () =>
|
||||||
|
withLocation(
|
||||||
|
{ plugins: ["-*"] },
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined()
|
||||||
|
expect(yield* agents.get(AgentV2.ID.make("folder"))).toBeUndefined()
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("loads user plugins before internal post plugins", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugins = yield* PluginV2.Service
|
const sdk = yield* SdkPlugins.Service
|
||||||
const agents = yield* AgentV2.Service
|
yield* sdk.register(define({ id: "sdk-order", effect: () => Effect.void }))
|
||||||
const fs = yield* FSUtil.Service
|
yield* withLocation(
|
||||||
const location = yield* Location.Service
|
{
|
||||||
const npm = yield* Npm.Service
|
plugins: [
|
||||||
const host = yield* PluginHost.make(plugins)
|
path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||||
const document = path.join(import.meta.dir, "opencode.json")
|
path.join(import.meta.dir, "../plugin/fixtures/variant-source-plugin.ts"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ready()
|
||||||
|
const registry = yield* PluginV2.Service
|
||||||
|
const ids = (yield* registry.list()).map((plugin) => String(plugin.id))
|
||||||
|
expect(ids.indexOf("opencode.agent")).toBeLessThan(ids.indexOf("sdk-order"))
|
||||||
|
expect(ids.indexOf("sdk-order")).toBeLessThan(ids.indexOf("config-promise-plugin"))
|
||||||
|
expect(ids.indexOf("config-promise-plugin")).toBeLessThan(ids.indexOf("variant-source"))
|
||||||
|
expect(ids.indexOf("variant-source")).toBeLessThan(ids.indexOf("opencode.config.provider"))
|
||||||
|
expect(ids.indexOf("opencode.config.provider")).toBeLessThan(ids.indexOf("opencode.variant"))
|
||||||
|
|
||||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
const catalog = yield* Catalog.Service
|
||||||
Effect.provideService(PluginV2.Service, plugins),
|
expect(
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
(yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants,
|
||||||
Effect.provideService(Location.Service, location),
|
).toEqual([
|
||||||
Effect.provideService(Npm.Service, npm),
|
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
|
||||||
Effect.provideService(
|
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
|
||||||
Config.Service,
|
])
|
||||||
Config.Service.of({
|
}),
|
||||||
entries: () =>
|
|
||||||
Effect.succeed([
|
|
||||||
new Config.Document({
|
|
||||||
type: "document",
|
|
||||||
path: document,
|
|
||||||
info: decode({
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
package: "../plugin/fixtures/config-promise-plugin.ts",
|
|
||||||
options: { description: "Loaded from config" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
|
||||||
description: "Loaded from config",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("loads a configured Effect plugin with options", () =>
|
it.live("allows variant generation to be disabled", () =>
|
||||||
Effect.gen(function* () {
|
withLocation(
|
||||||
const plugins = yield* PluginV2.Service
|
{
|
||||||
const agents = yield* AgentV2.Service
|
plugins: [path.join(import.meta.dir, "../plugin/fixtures/variant-source-plugin.ts"), "-opencode.variant"],
|
||||||
const fs = yield* FSUtil.Service
|
},
|
||||||
const location = yield* Location.Service
|
Effect.gen(function* () {
|
||||||
const npm = yield* Npm.Service
|
yield* ready()
|
||||||
const host = yield* PluginHost.make(plugins)
|
const registry = yield* PluginV2.Service
|
||||||
|
expect((yield* registry.list()).map((plugin) => String(plugin.id))).not.toContain("opencode.variant")
|
||||||
|
|
||||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
const catalog = yield* Catalog.Service
|
||||||
Effect.provideService(PluginV2.Service, plugins),
|
expect(
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
(yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants,
|
||||||
Effect.provideService(Location.Service, location),
|
).toEqual([expect.objectContaining({ id: "high", headers: { custom: "true" } })])
|
||||||
Effect.provideService(Npm.Service, npm),
|
}),
|
||||||
Effect.provideService(
|
),
|
||||||
Config.Service,
|
|
||||||
Config.Service.of({
|
|
||||||
entries: () =>
|
|
||||||
Effect.succeed([
|
|
||||||
new Config.Document({
|
|
||||||
type: "document",
|
|
||||||
path: path.join(import.meta.dir, "opencode.json"),
|
|
||||||
info: decode({
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
package: "../plugin/fixtures/config-effect-plugin.ts",
|
|
||||||
options: { description: "Effect plugin from config" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(yield* waitForAgent(agents, "effect-configured")).toMatchObject({
|
|
||||||
description: "Effect plugin from config",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("ignores invalid plugins and continues loading", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugins = yield* PluginV2.Service
|
|
||||||
const agents = yield* AgentV2.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const host = yield* PluginHost.make(plugins)
|
|
||||||
|
|
||||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
|
||||||
Effect.provideService(PluginV2.Service, plugins),
|
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
|
||||||
Effect.provideService(Location.Service, location),
|
|
||||||
Effect.provideService(Npm.Service, npm),
|
|
||||||
Effect.provideService(
|
|
||||||
Config.Service,
|
|
||||||
Config.Service.of({
|
|
||||||
entries: () =>
|
|
||||||
Effect.succeed([
|
|
||||||
new Config.Document({
|
|
||||||
type: "document",
|
|
||||||
path: path.join(import.meta.dir, "opencode.json"),
|
|
||||||
info: decode({
|
|
||||||
plugins: [
|
|
||||||
"../plugin/fixtures/missing-plugin.ts",
|
|
||||||
"../plugin/fixtures/invalid-plugin.ts",
|
|
||||||
{
|
|
||||||
package: "../plugin/fixtures/config-promise-plugin.ts",
|
|
||||||
options: { description: "Loaded after invalid plugins" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
|
||||||
description: "Loaded after invalid plugins",
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("installs and resolves npm plugin packages", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugins = yield* PluginV2.Service
|
|
||||||
const agents = yield* AgentV2.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const host = yield* PluginHost.make(plugins)
|
|
||||||
let installed: string | undefined
|
|
||||||
const npm = Npm.Service.of({
|
|
||||||
add: (spec) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
installed = spec
|
|
||||||
return {
|
|
||||||
directory: import.meta.dir,
|
|
||||||
entrypoint: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
install: () => Effect.void,
|
|
||||||
which: () => Effect.succeed(undefined),
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
|
||||||
Effect.provideService(PluginV2.Service, plugins),
|
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
|
||||||
Effect.provideService(Location.Service, location),
|
|
||||||
Effect.provideService(Npm.Service, npm),
|
|
||||||
Effect.provideService(
|
|
||||||
Config.Service,
|
|
||||||
Config.Service.of({
|
|
||||||
entries: () =>
|
|
||||||
Effect.succeed([
|
|
||||||
new Config.Document({
|
|
||||||
type: "document",
|
|
||||||
info: decode({
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
package: "example-plugin@1.0.0",
|
|
||||||
options: { description: "Installed from npm" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
|
||||||
description: "Installed from npm",
|
|
||||||
})
|
|
||||||
expect(installed).toBe("example-plugin@1.0.0")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("loads plugin files from config directories", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugins = yield* PluginV2.Service
|
|
||||||
const agents = yield* AgentV2.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const location = yield* Location.Service
|
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const host = yield* PluginHost.make(plugins)
|
|
||||||
|
|
||||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
|
||||||
Effect.provideService(PluginV2.Service, plugins),
|
|
||||||
Effect.provideService(FSUtil.Service, fs),
|
|
||||||
Effect.provideService(Location.Service, location),
|
|
||||||
Effect.provideService(Npm.Service, npm),
|
|
||||||
Effect.provideService(
|
|
||||||
Config.Service,
|
|
||||||
Config.Service.of({
|
|
||||||
entries: () =>
|
|
||||||
Effect.succeed([
|
|
||||||
new Config.Directory({
|
|
||||||
type: "directory",
|
|
||||||
path: AbsolutePath.make(path.join(import.meta.dir, "fixtures")),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(yield* waitForAgent(agents, "directory")).toMatchObject({
|
|
||||||
description: "Loaded from plugin directory",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
expect(yield* waitForAgent(agents, "folder")).toMatchObject({
|
|
||||||
description: "Loaded from plugin folder",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) {
|
const ready = Effect.fnUntraced(function* () {
|
||||||
for (let attempt = 0; attempt < 100; attempt++) {
|
const supervisor = yield* PluginSupervisor.Service
|
||||||
const agent = yield* agents.get(AgentV2.ID.make(id))
|
yield* supervisor.ready
|
||||||
if (agent) return agent
|
|
||||||
yield* Effect.sleep("10 millis")
|
|
||||||
}
|
|
||||||
return yield* Effect.die(`Timed out waiting for agent ${id}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function withLocation<A, E, R>(config: unknown, effect: Effect.Effect<A, E, R>, fixtures = false) {
|
||||||
|
return Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.tap((tmp) =>
|
||||||
|
Effect.promise(async () => {
|
||||||
|
if (fixtures) {
|
||||||
|
const directory = path.join(tmp.path, ".opencode")
|
||||||
|
await fs.mkdir(directory, { recursive: true })
|
||||||
|
await Promise.all(
|
||||||
|
["plugin", "plugins"].map((name) =>
|
||||||
|
fs.symlink(path.join(import.meta.dir, "fixtures", name), path.join(directory, name), "dir"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (config !== undefined) {
|
||||||
|
const directory = fixtures ? path.join(tmp.path, ".opencode") : tmp.path
|
||||||
|
await fs.mkdir(directory, { recursive: true })
|
||||||
|
await fs.writeFile(path.join(directory, "opencode.json"), JSON.stringify(config))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Effect.flatMap((tmp) =>
|
||||||
|
effect.pipe(
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { CommandV2 } from "@opencode-ai/core/command"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||||
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
|
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
|
||||||
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
|
|
||||||
import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
|
import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
|
||||||
import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference"
|
import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference"
|
||||||
import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill"
|
import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill"
|
||||||
|
|
@ -37,7 +36,7 @@ describe("config plugin reloads", () => {
|
||||||
const references = yield* Reference.Service
|
const references = yield* Reference.Service
|
||||||
const skills = yield* SkillV2.Service
|
const skills = yield* SkillV2.Service
|
||||||
const host = yield* PluginHost.make(plugins)
|
const host = yield* PluginHost.make(plugins)
|
||||||
let entries: Config.Entry[] = [config("first", "First plugin")]
|
let entries: Config.Entry[] = [config("first")]
|
||||||
const service = Config.Service.of({ entries: () => Effect.sync(() => entries) })
|
const service = Config.Service.of({ entries: () => Effect.sync(() => entries) })
|
||||||
const setup = <R>(effect: Effect.Effect<void, never, R>) =>
|
const setup = <R>(effect: Effect.Effect<void, never, R>) =>
|
||||||
effect.pipe(Effect.provideService(Config.Service, service))
|
effect.pipe(Effect.provideService(Config.Service, service))
|
||||||
|
|
@ -47,7 +46,6 @@ describe("config plugin reloads", () => {
|
||||||
yield* setup(ConfigSkillPlugin.Plugin.effect(host))
|
yield* setup(ConfigSkillPlugin.Plugin.effect(host))
|
||||||
yield* setup(ConfigReferencePlugin.Plugin.effect(host))
|
yield* setup(ConfigReferencePlugin.Plugin.effect(host))
|
||||||
yield* setup(ConfigProviderPlugin.Plugin.effect(host))
|
yield* setup(ConfigProviderPlugin.Plugin.effect(host))
|
||||||
yield* setup(ConfigExternalPlugin.Plugin.effect(host))
|
|
||||||
|
|
||||||
expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent")
|
expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent")
|
||||||
expect((yield* commands.get("first"))?.description).toBe("First command")
|
expect((yield* commands.get("first"))?.description).toBe("First command")
|
||||||
|
|
@ -56,9 +54,8 @@ describe("config plugin reloads", () => {
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
|
expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
|
||||||
expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined()
|
expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined()
|
||||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("First plugin")
|
|
||||||
|
|
||||||
entries = [config("second", "Second plugin")]
|
entries = [config("second")]
|
||||||
yield* events.publish(ConfigSchema.Event.Updated, {})
|
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||||
yield* waitUntil(
|
yield* waitUntil(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -80,12 +77,11 @@ describe("config plugin reloads", () => {
|
||||||
expect(
|
expect(
|
||||||
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
|
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("First plugin")
|
|
||||||
}).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
|
}).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function config(name: string, pluginDescription?: string) {
|
function config(name: string) {
|
||||||
return new Config.Document({
|
return new Config.Document({
|
||||||
type: "document",
|
type: "document",
|
||||||
path: document,
|
path: document,
|
||||||
|
|
@ -95,15 +91,6 @@ function config(name: string, pluginDescription?: string) {
|
||||||
skills: [`/skills/${name}`],
|
skills: [`/skills/${name}`],
|
||||||
references: { [name]: `/references/${name}` },
|
references: { [name]: `/references/${name}` },
|
||||||
providers: { [name]: { models: { chat: { name: `${title(name)} model` } } } },
|
providers: { [name]: { models: { chat: { name: `${title(name)} model` } } } },
|
||||||
plugins:
|
|
||||||
pluginDescription === undefined
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
package: "../plugin/fixtures/config-promise-plugin.ts",
|
|
||||||
options: { description: pluginDescription },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { DateTime, Effect, Equal, Hash, Schema } from "effect"
|
import { Config } from "@opencode-ai/schema/config"
|
||||||
|
import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
|
@ -10,6 +11,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
|
@ -27,6 +29,124 @@ import { ToolRegistry } from "../src/tool/registry"
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
||||||
|
|
||||||
describe("LocationServiceMap", () => {
|
describe("LocationServiceMap", () => {
|
||||||
|
it.live("applies ordered plugin config operations during boot", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.writeFile(path.join(dir.path, "opencode.json"), JSON.stringify({ plugins: ["-*", "opencode.agent"] })),
|
||||||
|
)
|
||||||
|
const plugins = yield* Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
yield* (yield* PluginSupervisor.Service).ready
|
||||||
|
return yield* plugins.list()
|
||||||
|
}).pipe(
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.provide(
|
||||||
|
LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(plugins.map((plugin) => plugin.id)).toEqual([PluginV2.ID.make("opencode.agent")])
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("reloads the plugin generation after config updates", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const file = path.join(dir.path, "opencode.json")
|
||||||
|
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] })))
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const registry = yield* PluginV2.Service
|
||||||
|
const supervisor = yield* PluginSupervisor.Service
|
||||||
|
yield* supervisor.ready
|
||||||
|
expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"])
|
||||||
|
|
||||||
|
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.command"] })))
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if ((yield* registry.list()).some((plugin) => plugin.id === "opencode.command")) break
|
||||||
|
yield* Effect.sleep("20 millis")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.command"])
|
||||||
|
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.writeFile(
|
||||||
|
file,
|
||||||
|
JSON.stringify({
|
||||||
|
plugins: ["-*", path.join(import.meta.dir, "plugin/fixtures/failing-plugin.ts")],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if ((yield* registry.list()).length === 0) break
|
||||||
|
yield* Effect.sleep("20 millis")
|
||||||
|
}
|
||||||
|
expect(yield* registry.list()).toEqual([])
|
||||||
|
|
||||||
|
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] })))
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if ((yield* registry.list()).some((plugin) => plugin.id === "opencode.agent")) break
|
||||||
|
yield* Effect.sleep("20 millis")
|
||||||
|
}
|
||||||
|
expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"])
|
||||||
|
}).pipe(
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.provide(
|
||||||
|
LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("routes located events only to their location", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
|
||||||
|
(dirs) => Effect.promise(() => Promise.all(dirs.map((dir) => dir[Symbol.asyncDispose]())).then(() => undefined)),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap(([first, second]) =>
|
||||||
|
Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const firstRef = Location.Ref.make({ directory: AbsolutePath.make(first.path) })
|
||||||
|
const secondRef = Location.Ref.make({ directory: AbsolutePath.make(second.path) })
|
||||||
|
const firstContext = yield* locations.contextEffect(firstRef)
|
||||||
|
const secondContext = yield* locations.contextEffect(secondRef)
|
||||||
|
const received = { first: 0, second: 0 }
|
||||||
|
yield* events.subscribe(Config.Event.Updated).pipe(
|
||||||
|
Stream.runForEach(() => Effect.sync(() => received.first++)),
|
||||||
|
Effect.provideContext(firstContext),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
yield* events.subscribe(Config.Event.Updated).pipe(
|
||||||
|
Stream.runForEach(() => Effect.sync(() => received.second++)),
|
||||||
|
Effect.provideContext(secondContext),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
|
|
||||||
|
yield* events.publish(Config.Event.Updated, {}, { location: firstRef })
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
|
|
||||||
|
expect(received).toEqual({ first: 1, second: 0 })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("reuses cached services for constructed and decoded location refs", () =>
|
it.live("reuses cached services for constructed and decoded location refs", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|
@ -64,7 +184,7 @@ describe("LocationServiceMap", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
|
yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
// Tool plugins register during the forked PluginInternal boot; wait for
|
// Tool plugins register during the forked PluginSupervisor boot; wait for
|
||||||
// every expected tool rather than relying on batch ordering.
|
// every expected tool rather than relying on batch ordering.
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
[
|
[
|
||||||
|
|
@ -257,7 +377,7 @@ describe("LocationServiceMap", () => {
|
||||||
})
|
})
|
||||||
.pipe(Effect.asVoid),
|
.pipe(Effect.asVoid),
|
||||||
})
|
})
|
||||||
yield* plugins.add(PluginV2.ID.make(reviewer.id), reviewer.effect)
|
yield* plugins.activate([{ id: PluginV2.ID.make(reviewer.id), effect: reviewer.effect }])
|
||||||
|
|
||||||
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||||
description: "Reviews code",
|
description: "Reviews code",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Fiber, Schema, Stream } from "effect"
|
import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
|
@ -16,6 +16,8 @@ import { PluginTestLayer } from "./plugin/fixture"
|
||||||
|
|
||||||
const it = testEffect(PluginTestLayer)
|
const it = testEffect(PluginTestLayer)
|
||||||
|
|
||||||
|
class Secret extends Context.Service<Secret, string>()("@opencode/test/PluginSecret") {}
|
||||||
|
|
||||||
describe("PluginV2", () => {
|
describe("PluginV2", () => {
|
||||||
it.live("exposes public events through the plugin context", () =>
|
it.live("exposes public events through the plugin context", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -35,43 +37,15 @@ describe("PluginV2", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("waits for a plugin and returns immediately once active", () =>
|
it.effect("skips identical generations and replaces changed plugin IDs", () =>
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugins = yield* PluginV2.Service
|
|
||||||
const id = PluginV2.ID.make("waited")
|
|
||||||
const waiting = yield* plugins.wait(id).pipe(Effect.forkChild)
|
|
||||||
|
|
||||||
yield* plugins.add(id, () => Effect.void)
|
|
||||||
yield* Fiber.join(waiting)
|
|
||||||
yield* plugins.wait(id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("propagates plugin activation defects to waiters", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugins = yield* PluginV2.Service
|
|
||||||
const id = PluginV2.ID.make("failed")
|
|
||||||
const waiting = yield* plugins.wait(id).pipe(Effect.exit, Effect.forkChild)
|
|
||||||
|
|
||||||
const added = yield* plugins.add(id, () => Effect.die("boom")).pipe(Effect.exit)
|
|
||||||
const pending = yield* Fiber.join(waiting)
|
|
||||||
const later = yield* plugins.wait(id).pipe(Effect.exit)
|
|
||||||
|
|
||||||
expect(Exit.isFailure(added)).toBe(true)
|
|
||||||
expect(Exit.isFailure(pending)).toBe(true)
|
|
||||||
expect(Exit.isFailure(later)).toBe(true)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("adds, replaces, and removes plugins", () =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugins = yield* PluginV2.Service
|
const plugins = yield* PluginV2.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
let description = "first"
|
let description = "first"
|
||||||
|
|
||||||
const managed = () =>
|
const managed = (id: string) =>
|
||||||
define({
|
define({
|
||||||
id: "managed",
|
id,
|
||||||
effect: (ctx) =>
|
effect: (ctx) =>
|
||||||
ctx.agent
|
ctx.agent
|
||||||
.transform((agents) =>
|
.transform((agents) =>
|
||||||
|
|
@ -82,19 +56,101 @@ describe("PluginV2", () => {
|
||||||
.pipe(Effect.asVoid),
|
.pipe(Effect.asVoid),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
yield* plugins.activate([managed("managed")])
|
||||||
|
|
||||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||||
|
|
||||||
description = "second"
|
description = "second"
|
||||||
yield* plugins.add(PluginV2.ID.make("managed"), managed().effect)
|
yield* plugins.activate([managed("managed")])
|
||||||
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||||
|
|
||||||
|
yield* plugins.activate([managed("managed-next")])
|
||||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||||
|
|
||||||
yield* plugins.remove(PluginV2.ID.make("managed"))
|
yield* plugins.activate([])
|
||||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("rejects duplicate IDs before replacing the active generation", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const active = PluginV2.ID.make("active")
|
||||||
|
const duplicate = PluginV2.ID.make("duplicate")
|
||||||
|
yield* plugins.activate([{ id: active, effect: () => Effect.void }])
|
||||||
|
|
||||||
|
const result = yield* plugins
|
||||||
|
.activate([
|
||||||
|
{ id: duplicate, effect: () => Effect.void },
|
||||||
|
{ id: duplicate, effect: () => Effect.void },
|
||||||
|
])
|
||||||
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
|
expect(yield* plugins.list()).toEqual([{ id: active }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("retries the same generation after materialization fails", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
let fail = true
|
||||||
|
const plugin = define({
|
||||||
|
id: "retry",
|
||||||
|
effect: (ctx) =>
|
||||||
|
ctx.agent
|
||||||
|
.transform(() => {
|
||||||
|
if (fail) throw new Error("materialization failed")
|
||||||
|
})
|
||||||
|
.pipe(Effect.asVoid),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(Exit.isFailure(yield* plugins.activate([plugin]).pipe(Effect.exit))).toBe(true)
|
||||||
|
fail = false
|
||||||
|
yield* plugins.activate([plugin])
|
||||||
|
|
||||||
|
expect(yield* plugins.list()).toEqual([{ id: PluginV2.ID.make("retry") }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("closes the previous generation in reverse order", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const closed: string[] = []
|
||||||
|
yield* plugins.activate(
|
||||||
|
["first", "second"].map((id) => ({
|
||||||
|
id: PluginV2.ID.make(id),
|
||||||
|
effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* plugins.activate([])
|
||||||
|
|
||||||
|
expect(closed).toEqual(["second", "first"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("isolates plugins from ambient services", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
let visible = true
|
||||||
|
const plugin = define({
|
||||||
|
id: "isolated",
|
||||||
|
effect: () =>
|
||||||
|
Effect.serviceOption(Secret).pipe(
|
||||||
|
Effect.tap((secret) => Effect.sync(() => (visible = secret._tag === "Some"))),
|
||||||
|
Effect.asVoid,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* plugins
|
||||||
|
.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
|
||||||
|
.pipe(Effect.provideService(Secret, "secret"))
|
||||||
|
|
||||||
|
expect(visible).toBe(false)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("registers location tools through the plugin context", () =>
|
it.effect("registers location tools through the plugin context", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugins = yield* PluginV2.Service
|
const plugins = yield* PluginV2.Service
|
||||||
|
|
@ -114,12 +170,12 @@ describe("PluginV2", () => {
|
||||||
.pipe(Effect.orDie),
|
.pipe(Effect.orDie),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* plugins.add(PluginV2.ID.make(plugin.id), plugin.effect)
|
yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
|
||||||
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
|
||||||
"plugin_tool",
|
"plugin_tool",
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* plugins.remove(PluginV2.ID.make(plugin.id))
|
yield* plugins.activate([])
|
||||||
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).not.toContain(
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).not.toContain(
|
||||||
"plugin_tool",
|
"plugin_tool",
|
||||||
)
|
)
|
||||||
|
|
@ -149,7 +205,7 @@ describe("PluginV2", () => {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* plugins.add(PluginV2.ID.make(plugin.id), plugin.effect)
|
yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
|
||||||
|
|
||||||
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toEqual([
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toEqual([
|
||||||
"plain",
|
"plain",
|
||||||
|
|
@ -201,7 +257,7 @@ describe("PluginV2", () => {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* plugins.add(PluginV2.ID.make(plugin.id), plugin.effect)
|
yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
|
||||||
|
|
||||||
const materialized = yield* registry.materialize({ model: testModel })
|
const materialized = yield* registry.materialize({ model: testModel })
|
||||||
const settlement = yield* materialized.settle({
|
const settlement = yield* materialized.settle({
|
||||||
|
|
|
||||||
7
packages/core/test/plugin/fixtures/failing-plugin.ts
Normal file
7
packages/core/test/plugin/fixtures/failing-plugin.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
id: "failing-plugin",
|
||||||
|
effect: () => Effect.die("plugin failed"),
|
||||||
|
})
|
||||||
29
packages/core/test/plugin/fixtures/variant-source-plugin.ts
Normal file
29
packages/core/test/plugin/fixtures/variant-source-plugin.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
export default define({
|
||||||
|
id: "variant-source",
|
||||||
|
effect: (ctx) =>
|
||||||
|
ctx.catalog
|
||||||
|
.transform((catalog) => {
|
||||||
|
catalog.provider.update("configured", (provider) => {
|
||||||
|
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||||
|
})
|
||||||
|
catalog.model.update("configured", "glm-5.2", (model) => {
|
||||||
|
model.api = {
|
||||||
|
id: "glm-5.2",
|
||||||
|
type: "aisdk",
|
||||||
|
package: "@ai-sdk/openai-compatible",
|
||||||
|
}
|
||||||
|
model.variants = [
|
||||||
|
{
|
||||||
|
id: "high",
|
||||||
|
settings: {},
|
||||||
|
headers: { custom: "true" },
|
||||||
|
body: {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.pipe(Effect.asVoid),
|
||||||
|
})
|
||||||
|
|
@ -59,8 +59,6 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||||
},
|
},
|
||||||
plugin: overrides.plugin ?? {
|
plugin: overrides.plugin ?? {
|
||||||
list: () => Effect.die("unused plugin.list"),
|
list: () => Effect.die("unused plugin.list"),
|
||||||
add: () => Effect.die("unused plugin.add"),
|
|
||||||
remove: () => Effect.die("unused plugin.remove"),
|
|
||||||
},
|
},
|
||||||
reference: overrides.reference ?? {
|
reference: overrides.reference ?? {
|
||||||
list: () => Effect.die("unused reference.list"),
|
list: () => Effect.die("unused reference.list"),
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const addPlugin = Effect.fn(function* () {
|
||||||
|
|
||||||
describe("KiloPlugin", () => {
|
describe("KiloPlugin", () => {
|
||||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("kilo"))),
|
Effect.sync(() =>
|
||||||
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.kilo")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies legacy referer headers only to kilo", () =>
|
it.effect("applies legacy referer headers only to kilo", () =>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ const addPlugin = Effect.fn(function* () {
|
||||||
|
|
||||||
describe("LLMGatewayPlugin", () => {
|
describe("LLMGatewayPlugin", () => {
|
||||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("llmgateway"))),
|
Effect.sync(() =>
|
||||||
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.llmgateway")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies legacy referer headers only to enabled llmgateway", () =>
|
it.effect("applies legacy referer headers only to enabled llmgateway", () =>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const addPlugin = Effect.fn(function* () {
|
||||||
|
|
||||||
describe("NvidiaPlugin", () => {
|
describe("NvidiaPlugin", () => {
|
||||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("nvidia"))),
|
Effect.sync(() =>
|
||||||
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.nvidia")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies NVIDIA tracking headers only to nvidia", () =>
|
it.effect("applies NVIDIA tracking headers only to nvidia", () =>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ const addPlugin = Effect.fn(function* () {
|
||||||
|
|
||||||
describe("OpenRouterPlugin", () => {
|
describe("OpenRouterPlugin", () => {
|
||||||
it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
|
it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("openrouter"))),
|
Effect.sync(() =>
|
||||||
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.openrouter")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies legacy referer headers only to openrouter", () =>
|
it.effect("applies legacy referer headers only to openrouter", () =>
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,11 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
||||||
describe("SnowflakeCortexPlugin", () => {
|
describe("SnowflakeCortexPlugin", () => {
|
||||||
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
|
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("snowflake-cortex"))
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.snowflake-cortex"))
|
||||||
const ids = ProviderPlugins.map((p) => p.id)
|
const ids = ProviderPlugins.map((p) => p.id)
|
||||||
expect(ids.indexOf("snowflake-cortex")).toBeLessThan(ids.indexOf("openai-compatible"))
|
expect(ids.indexOf("opencode.provider.snowflake-cortex")).toBeLessThan(
|
||||||
|
ids.indexOf("opencode.provider.openai-compatible"),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ function required<T>(value: T | undefined): T {
|
||||||
|
|
||||||
describe("ZenmuxPlugin", () => {
|
describe("ZenmuxPlugin", () => {
|
||||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("zenmux"))),
|
Effect.sync(() =>
|
||||||
|
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.zenmux")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies the exact legacy Zenmux headers", () =>
|
it.effect("applies the exact legacy Zenmux headers", () =>
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
||||||
|
|
||||||
export const Info = Schema.Struct({
|
export const Info = Schema.Struct({
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
|
|
@ -101,7 +101,7 @@ const layer = Layer.effect(
|
||||||
const skillDirs = yield* skill.dirs()
|
const skillDirs = yield* skill.dirs()
|
||||||
const referenceDirs = Object.keys(cfg.references ?? cfg.reference ?? {}).length
|
const referenceDirs = Object.keys(cfg.references ?? cfg.reference ?? {}).length
|
||||||
? yield* Effect.gen(function* () {
|
? yield* Effect.gen(function* () {
|
||||||
yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
|
yield* (yield* PluginSupervisor.Service).ready
|
||||||
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
|
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
|
||||||
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
||||||
: []
|
: []
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,4 @@ export function define<R = Scope.Scope>(plugin: Plugin<R>) {
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDomain extends PluginApi<unknown> {
|
export interface PluginDomain extends PluginApi<unknown> {}
|
||||||
readonly add: (plugin: Plugin) => Effect.Effect<void>
|
|
||||||
readonly remove: (id: string) => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,4 @@ export function define(plugin: Plugin) {
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDomain extends PluginApi {
|
export interface PluginDomain extends PluginApi {}
|
||||||
readonly add: (plugin: Plugin) => Promise<void>
|
|
||||||
readonly remove: (id: string) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue