mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-11 12:10:01 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71f7b354d6 | |||
| 0652e32ae1 | |||
| e0a9d43f8c | |||
| 4ee4966d05 |
@@ -4,7 +4,7 @@ import { Directory, Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigPlugin } from "@opencode-ai/schema/config/plugin"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Context, Effect, Layer, Option, PubSub, Schema, Scope, Stream } from "effect"
|
||||
import { Context, Effect, Layer, Option, PubSub, Scope, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Config } from "../../config"
|
||||
@@ -154,12 +154,6 @@ const scan = Effect.fn("ConfigPluginSource.scan")(function* (
|
||||
})
|
||||
|
||||
const sourceDirectories = ["plugin", "plugins"] as const
|
||||
const Package = Schema.Struct({
|
||||
exports: Schema.optional(Schema.Unknown),
|
||||
module: Schema.optional(Schema.Unknown),
|
||||
main: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
const decodePackage = Schema.decodeUnknownOption(Package)
|
||||
|
||||
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -172,38 +166,7 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||
symlink: true,
|
||||
})
|
||||
.pipe(Effect.orElseSucceed(() => []))
|
||||
const children = yield* fs
|
||||
.scan(`{${sourceDirectories.join(",")}}/*`, {
|
||||
cwd: directory,
|
||||
absolute: true,
|
||||
include: "all",
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})
|
||||
.pipe(Effect.orElseSucceed(() => []))
|
||||
const directories = yield* Effect.filter(children.sort(), fs.isDir)
|
||||
const packages = yield* Effect.forEach(directories, (child) => discoverPackage(fs, child))
|
||||
return [...files.sort(), ...packages.filter((target): target is string => typeof target === "string")].map(
|
||||
(target): Operation => ({ type: "add", target, options: {} }),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function discoverPackage(fs: FSUtil.Interface, directory: string) {
|
||||
return Effect.gen(function* () {
|
||||
const manifest = yield* fs
|
||||
.readJson(path.join(directory, "package.json"))
|
||||
.pipe(Effect.map(decodePackage), Effect.orElseSucceed(Option.none))
|
||||
const configured = Option.isSome(manifest)
|
||||
? [manifest.value.exports, manifest.value.module, manifest.value.main].filter(
|
||||
(entry): entry is string => typeof entry === "string",
|
||||
)
|
||||
: []
|
||||
const target = yield* Effect.findFirst(
|
||||
[...configured, "index.ts", "index.js"].map((entry) => path.resolve(directory, entry)),
|
||||
fs.isFile,
|
||||
)
|
||||
return Option.getOrUndefined(target)
|
||||
return files.sort().map((target): Operation => ({ type: "add", target, options: {} }))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -101,16 +101,6 @@ export const Plugin = define({
|
||||
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("plan"), (item) => {
|
||||
item.name = Agent.Name.make("Plan")
|
||||
item.description = "Plan mode. Disallows all edit tools."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
{ action: "question", resource: "*", effect: "allow" },
|
||||
{ action: "edit", resource: "*", effect: "deny" },
|
||||
)
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("general"), (item) => {
|
||||
item.name = Agent.Name.make("General")
|
||||
item.description =
|
||||
|
||||
@@ -60,6 +60,7 @@ import { WellKnown } from "../wellknown"
|
||||
import { WriteTool } from "../tool/plugin/write"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { CommandPlugin } from "./command"
|
||||
import { PlanPlugin } from "./plan"
|
||||
import { ModelsDevPlugin } from "./models-dev"
|
||||
import { ProviderPlugins } from "./provider"
|
||||
import { WebSearchPlugins } from "./websearch"
|
||||
@@ -192,6 +193,7 @@ export type InternalPlugin = Plugin<Requirements | Scope.Scope>
|
||||
const pre = [
|
||||
WellKnownPlugin.Plugin,
|
||||
AgentPlugin.Plugin,
|
||||
PlanPlugin.Plugin,
|
||||
CommandPlugin.Plugin,
|
||||
SkillPlugin.Plugin,
|
||||
...SystemPromptPlugin.Plugins,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export * as PlanPlugin from "./plan"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Agent } from "../agent"
|
||||
|
||||
const plan = Agent.ID.make("plan")
|
||||
|
||||
const enter = `<system-reminder>
|
||||
You are in Plan mode. This is a READ-ONLY environment. You are not allowed to edit files, and you may not ask a subagent to edit them either.
|
||||
</system-reminder>`
|
||||
|
||||
const leave = `<system-reminder>
|
||||
You are no longer in Plan mode. The previous read-only restrictions no longer apply. You may edit files again.
|
||||
</system-reminder>`
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.plan",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.agent.transform((draft) => {
|
||||
draft.update(plan, (item) => {
|
||||
item.name = Agent.Name.make("Plan")
|
||||
item.description = "Read-only agent for exploring the codebase and planning work before implementation."
|
||||
item.mode = "primary"
|
||||
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
|
||||
})
|
||||
})
|
||||
|
||||
yield* ctx.tool.hook("execute.before", (event) => {
|
||||
if (event.agent !== plan) return Effect.void
|
||||
if (event.tool !== "edit" && event.tool !== "write" && event.tool !== "patch") return Effect.void
|
||||
return new ToolFailure({
|
||||
message: `Cannot use ${event.tool} in Plan mode. You are in a read-only mode and must not modify files.`,
|
||||
})
|
||||
})
|
||||
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "session.agent.selected"),
|
||||
Stream.runForEach((event) => {
|
||||
if (event.data.agent === event.data.previous) return Effect.void
|
||||
const text = event.data.agent === plan ? enter : event.data.previous === plan ? leave : undefined
|
||||
if (!text) return Effect.void
|
||||
return ctx.session
|
||||
.synthetic({
|
||||
sessionID: event.data.sessionID,
|
||||
text,
|
||||
resume: false,
|
||||
})
|
||||
.pipe(Effect.catch(() => Effect.void))
|
||||
}),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
@@ -165,7 +165,6 @@ describe("Agent", () => {
|
||||
"compaction",
|
||||
"explore",
|
||||
"general",
|
||||
"plan",
|
||||
"summary",
|
||||
"title",
|
||||
])
|
||||
|
||||
@@ -168,69 +168,6 @@ describe("PluginSupervisor config", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads auto-discovered plugin packages from package metadata", () =>
|
||||
withLocation(
|
||||
undefined,
|
||||
Effect.gen(function* () {
|
||||
yield* ready()
|
||||
const plugins = yield* Plugin.Service
|
||||
expect((yield* plugins.list()).map((plugin) => String(plugin.id))).toContain("package-metadata")
|
||||
}),
|
||||
false,
|
||||
async (directory) => {
|
||||
const plugin = path.join(directory, ".opencode", "plugins", "package-metadata")
|
||||
await fs.mkdir(plugin, { recursive: true })
|
||||
await fs.writeFile(path.join(plugin, "package.json"), JSON.stringify({ exports: "./entry.ts" }))
|
||||
await fs.writeFile(path.join(plugin, "entry.ts"), discoveredPlugin("package-metadata"))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads auto-discovered plugin packages from index fallback", () =>
|
||||
withLocation(
|
||||
undefined,
|
||||
Effect.gen(function* () {
|
||||
yield* ready()
|
||||
const plugins = yield* Plugin.Service
|
||||
expect((yield* plugins.list()).map((plugin) => String(plugin.id))).toContain("index-fallback")
|
||||
}),
|
||||
false,
|
||||
async (directory) => {
|
||||
const plugin = path.join(directory, ".opencode", "plugins", "index-fallback")
|
||||
await fs.mkdir(plugin, { recursive: true })
|
||||
await fs.writeFile(path.join(plugin, "index.js"), discoveredPlugin("index-fallback"))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("prefers package metadata over index fallback", () =>
|
||||
withLocation(
|
||||
undefined,
|
||||
Effect.gen(function* () {
|
||||
yield* ready()
|
||||
const plugins = yield* Plugin.Service
|
||||
const ids = (yield* plugins.list()).map((plugin) => String(plugin.id))
|
||||
expect(ids).toContain("metadata-precedence")
|
||||
expect(ids).not.toContain("module-collision")
|
||||
expect(ids).not.toContain("main-collision")
|
||||
expect(ids).not.toContain("index-collision")
|
||||
}),
|
||||
false,
|
||||
async (directory) => {
|
||||
const plugin = path.join(directory, ".opencode", "plugins", "collision")
|
||||
await fs.mkdir(plugin, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(plugin, "package.json"),
|
||||
JSON.stringify({ exports: "./entry.js", module: "./module.js", main: "./main.js" }),
|
||||
)
|
||||
await fs.writeFile(path.join(plugin, "entry.js"), discoveredPlugin("metadata-precedence"))
|
||||
await fs.writeFile(path.join(plugin, "module.js"), discoveredPlugin("module-collision"))
|
||||
await fs.writeFile(path.join(plugin, "main.js"), discoveredPlugin("main-collision"))
|
||||
await fs.writeFile(path.join(plugin, "index.js"), discoveredPlugin("index-collision"))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
staticIt.live("uses only internal and SDK plugins when the static source is wired", () =>
|
||||
Effect.gen(function* () {
|
||||
const sdk = yield* SdkPlugins.Service
|
||||
@@ -452,7 +389,3 @@ export default Plugin.define({
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
function discoveredPlugin(id: string) {
|
||||
return `export default { id: ${JSON.stringify(id)}, setup() {} }`
|
||||
}
|
||||
|
||||
@@ -14300,9 +14300,15 @@
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -14373,9 +14379,6 @@
|
||||
},
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "agent"],
|
||||
@@ -14444,9 +14447,6 @@
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "model"],
|
||||
|
||||
@@ -14300,9 +14300,15 @@
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -14373,9 +14379,6 @@
|
||||
},
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "agent"],
|
||||
@@ -14444,9 +14447,6 @@
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "model"],
|
||||
|
||||
@@ -14300,9 +14300,15 @@
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -14373,9 +14379,6 @@
|
||||
},
|
||||
"agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"previous": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "agent"],
|
||||
@@ -14444,9 +14447,6 @@
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"previous": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "model"],
|
||||
|
||||
Reference in New Issue
Block a user