diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index e59fefe08060..032611e33a01 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -29,6 +29,7 @@ import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } fro import { registerAdapter } from "@/control-plane/adapters" import type { WorkspaceAdapter } from "@/control-plane/types" import { RuntimeFlags } from "@/effect/runtime-flags" +import { Skill } from "../skill" const log = Log.create({ service: "plugin" }) @@ -115,6 +116,7 @@ export const layer = Layer.effect( const bus = yield* Bus.Service const config = yield* Config.Service const flags = yield* RuntimeFlags.Service + const skill = yield* Skill.Service const state = yield* InstanceState.make( Effect.fn("Plugin.state")(function* (ctx) { @@ -149,6 +151,23 @@ export const layer = Layer.effect( }, // @ts-expect-error $: typeof Bun === "undefined" ? undefined : Bun.$, + skills: { + all: () => + bridge.promise( + skill.all().pipe( + Effect.orDie, + Effect.map((items) => items.map((item) => ({ ...item, description: item.description ?? "" }))), + ), + ), + get: (name: string) => + bridge.promise( + skill.get(name).pipe( + Effect.orDie, + Effect.map((item) => (item ? { ...item, description: item.description ?? "" } : item)), + ), + ), + dirs: () => bridge.promise(skill.dirs().pipe(Effect.orDie)), + }, } for (const plugin of flags.disableDefaultPlugins ? [] : INTERNAL_PLUGINS) { @@ -291,6 +310,7 @@ export const layer = Layer.effect( export const defaultLayer = layer.pipe( Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer), + Layer.provide(Skill.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer), ) diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index d79e01c78867..9ab308d97a57 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -35,6 +35,7 @@ const configLayer = Config.layer.pipe( const pluginLayer = Plugin.layer.pipe( Layer.provide(Bus.layer), Layer.provide(configLayer), + Layer.provide(SkillTest.empty), Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), ) const agentLayer = Agent.layer.pipe( diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index c10957996e27..4e7b7aa7c040 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -10,6 +10,7 @@ import { Plugin } from "@/plugin" import { RuntimeFlags } from "@/effect/runtime-flags" import { Auth } from "@/auth" import { Bus } from "@/bus" +import { Skill } from "@/skill" import { TestConfig } from "../fixture/config" import { testEffect } from "../lib/effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -23,6 +24,7 @@ function layer(directory: string, plugins: string[]) { Plugin.layer.pipe( Layer.provide(Bus.layer), Layer.provide(RuntimeFlags.layer()), + Layer.provide(Skill.defaultLayer), Layer.provide( TestConfig.layer({ get: () => diff --git a/packages/opencode/test/plugin/cloudflare.test.ts b/packages/opencode/test/plugin/cloudflare.test.ts index 5fa410683582..514f54eb1e6a 100644 --- a/packages/opencode/test/plugin/cloudflare.test.ts +++ b/packages/opencode/test/plugin/cloudflare.test.ts @@ -11,6 +11,11 @@ const pluginInput = { }, serverUrl: new URL("https://example.com"), $: {} as never, + skills: { + all: async () => [], + get: async () => undefined, + dirs: async () => [], + }, } function makeHookInput(overrides: { providerID?: string; apiId?: string; reasoning?: boolean }) { diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 271bcde99b23..df6ab1c63b17 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -191,6 +191,11 @@ describe("plugin.codex", () => { }, serverUrl: new URL("https://example.com"), $: {} as never, + skills: { + all: async () => [], + get: async () => undefined, + dirs: async () => [], + }, }, { issuer: server.url.origin, diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index 939247f09b4e..08210335bbc3 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/packages/opencode/test/plugin/github-copilot-models.test.ts @@ -228,6 +228,11 @@ test("remaps fallback oauth model urls to the enterprise host", async () => { }, serverUrl: new URL("https://example.com"), $: {} as never, + skills: { + all: async () => [], + get: async () => undefined, + dirs: async () => [], + }, }) const models = await hooks.provider!.models!( diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index ad03d229f2db..92c342d7f882 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, spyOn } from "bun:test" +import { afterAll, afterEach, describe, expect, spyOn } from "bun:test" import { Effect, Layer } from "effect" import fs from "fs/promises" import path from "path" @@ -8,14 +8,33 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" +const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS +const configContent = process.env.OPENCODE_CONFIG_CONTENT +process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" +delete process.env.OPENCODE_CONFIG_CONTENT + const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Bus } = await import("../../src/bus") +const { Skill } = await import("../../src/skill") const { Npm } = await import("@opencode-ai/core/npm") const { TestConfig } = await import("../fixture/config") const { RuntimeFlags } = await import("../../src/effect/runtime-flags") +afterAll(() => { + if (disableDefault === undefined) { + delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS + } else { + process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault + } + if (configContent === undefined) { + delete process.env.OPENCODE_CONFIG_CONTENT + return + } + process.env.OPENCODE_CONFIG_CONTENT = configContent +}) + afterEach(async () => { await disposeAllInstances() }) @@ -48,6 +67,7 @@ function load(dir: string, flags?: Parameters[0]) { Plugin.layer.pipe( Layer.provide(Bus.layer), Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, ...flags })), + Layer.provide(Skill.defaultLayer), Layer.provide( TestConfig.layer({ get: () => diff --git a/packages/opencode/test/plugin/native-skills.test.ts b/packages/opencode/test/plugin/native-skills.test.ts new file mode 100644 index 000000000000..5343e276cd57 --- /dev/null +++ b/packages/opencode/test/plugin/native-skills.test.ts @@ -0,0 +1,109 @@ +import { afterEach, expect, test } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture" + +const disable = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS +const cfg = process.env.OPENCODE_CONFIG_DIR +const content = process.env.OPENCODE_CONFIG_CONTENT +const home = process.env.OPENCODE_TEST_HOME +process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" + +const { Effect } = await import("effect") +const { MessageID, SessionID } = await import("../../src/session/schema") +const { ToolRegistry } = await import("../../src/tool/registry") + +afterEach(async () => { + if (disable === undefined) delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS + else process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disable + if (cfg === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = cfg + if (content === undefined) delete process.env.OPENCODE_CONFIG_CONTENT + else process.env.OPENCODE_CONFIG_CONTENT = content + if (home === undefined) delete process.env.OPENCODE_TEST_HOME + else process.env.OPENCODE_TEST_HOME = home + await disposeAllInstances() +}) + +// Validates the OMO+superpowers coexistence pattern: +// A plugin captures ctx.skills during server(), then calls ctx.skills.all() +// LAZILY (via a tool execute) after plugin loading completes. +// Native skills must be visible in the lazy call. +test("plugin tool can lazily call ctx.skills.all() and see native skills", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create a native skill in the standard location + const skill = path.join(dir, ".opencode", "skill", "test-native") + await fs.mkdir(skill, { recursive: true }) + await Bun.write( + path.join(skill, "SKILL.md"), + ["---", "name: test-native", "description: A test native skill", "---", "", "# Test Native Skill", ""].join("\n"), + ) + + // Create a plugin that captures ctx.skills and exposes a tool + // whose execute calls ctx.skills.all() lazily + const plugin = path.join(dir, ".opencode", "plugin") + await fs.mkdir(plugin, { recursive: true }) + const out = JSON.stringify(path.join(dir, "skills-result.json")) + await Bun.write( + path.join(plugin, "skill-reader.ts"), + [ + 'import { writeFileSync } from "fs"', + "", + "export default {", + ' id: \"test.skill-reader\",', + " server: async (ctx) => {", + " // Capture ctx.skills — do NOT call all() during server()", + " const skills = ctx.skills", + " return {", + " tool: {", + " check_native_skills: {", + ' description: \"Returns native skills found via ctx.skills.all()\",', + " args: {},", + " execute: async () => {", + " // LAZY call — runs after server() has returned", + " const all = await skills.all()", + ` writeFileSync(${out}, JSON.stringify(all.map(s => s.name)))`, + ' return all.map(s => s.name).join(\", \")', + " }", + " }", + " }", + " }", + " },", + "}", + "", + ].join("\n"), + ) + }, + }) + + process.env.OPENCODE_TEST_HOME = tmp.path + process.env.OPENCODE_CONFIG_DIR = path.join(tmp.path, ".config") + delete process.env.OPENCODE_CONFIG_CONTENT + await fs.mkdir(process.env.OPENCODE_CONFIG_DIR, { recursive: true }) + + await Effect.runPromise( + Effect.gen(function* () { + const svc = yield* ToolRegistry.Service + const tools = yield* svc.all() + const tool = tools.find((t) => t.id === "check_native_skills") + if (!tool) throw new Error("Plugin tool not found") + yield* tool.execute( + {}, + { + sessionID: SessionID.make("ses_test"), + messageID: MessageID.make("msg_test"), + agent: "test", + abort: new AbortController().signal, + extra: {}, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + }).pipe(Effect.provide(ToolRegistry.defaultLayer), provideInstance(tmp.path)), + ) + + const names = (await Bun.file(path.join(tmp.path, "skills-result.json")).json()) as string[] + expect(names).toContain("test-native") +}, 30000) diff --git a/packages/opencode/test/plugin/trigger.test.ts b/packages/opencode/test/plugin/trigger.test.ts index 3716bc3aca5e..08f26d21f929 100644 --- a/packages/opencode/test/plugin/trigger.test.ts +++ b/packages/opencode/test/plugin/trigger.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { afterAll, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -17,6 +17,10 @@ import { testEffect } from "../lib/effect" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" +import { Skill } from "../../src/skill" + +const configContent = process.env.OPENCODE_CONFIG_CONTENT +delete process.env.OPENCODE_CONFIG_CONTENT const configLayer = Config.layer.pipe( Layer.provide(EffectFlock.defaultLayer), @@ -32,6 +36,7 @@ const it = testEffect( Plugin.layer.pipe( Layer.provide(Bus.layer), Layer.provide(configLayer), + Layer.provide(Skill.defaultLayer), Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), ), CrossSpawnSpawner.defaultLayer, @@ -39,6 +44,14 @@ const it = testEffect( ) const systemHook = "experimental.chat.system.transform" +afterAll(() => { + if (configContent === undefined) { + delete process.env.OPENCODE_CONFIG_CONTENT + return + } + process.env.OPENCODE_CONFIG_CONTENT = configContent +}) + function withProject(source: string, self: Effect.Effect) { return provideTmpdirInstance((dir) => Effect.gen(function* () { diff --git a/packages/opencode/test/plugin/workspace-adapter.test.ts b/packages/opencode/test/plugin/workspace-adapter.test.ts index 79964d3deeb7..aca4c1e3e13f 100644 --- a/packages/opencode/test/plugin/workspace-adapter.test.ts +++ b/packages/opencode/test/plugin/workspace-adapter.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect } from "bun:test" +import { afterAll, afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -26,6 +26,10 @@ import { testEffect } from "../lib/effect" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" +import { Skill } from "../../src/skill" + +const configContent = process.env.OPENCODE_CONFIG_CONTENT +delete process.env.OPENCODE_CONFIG_CONTENT const configLayer = Config.layer.pipe( Layer.provide(EffectFlock.defaultLayer), @@ -39,6 +43,7 @@ const configLayer = Config.layer.pipe( const pluginLayer = Plugin.layer.pipe( Layer.provide(Bus.layer), Layer.provide(configLayer), + Layer.provide(Skill.defaultLayer), Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), ) const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) @@ -60,6 +65,14 @@ afterEach(async () => { await disposeAllInstances() }) +afterAll(() => { + if (configContent === undefined) { + delete process.env.OPENCODE_CONFIG_CONTENT + return + } + process.env.OPENCODE_CONFIG_CONTENT = configContent +}) + describe("plugin.workspace", () => { it.live("plugin can install a workspace adapter", () => provideTmpdirInstance((dir) => diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 24b804819ed3..660af352e8c8 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -46,6 +46,7 @@ process.env["OPENCODE_TEST_HOME"] = testHome // Set test managed config directory to isolate tests from system managed settings const testManagedConfigDir = path.join(dir, "managed") process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir +delete process.env["OPENCODE_CONFIG_CONTENT"] // Write the cache version file to prevent global/index.ts from clearing the cache const cacheDir = path.join(dir, "cache", "opencode") diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index fc1f6bff6ae7..52184da2da22 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -8,6 +8,10 @@ import { Config } from "../../src/config/config" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Global } from "@opencode-ai/core/global" +import { ProviderID, ModelID } from "../../src/provider/schema" +import { SessionID, MessageID } from "../../src/session/schema" +import { Tool } from "../../src/tool/tool" +import { ToolRegistry } from "../../src/tool/registry" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import path from "path" @@ -42,6 +46,18 @@ const itWithoutExternalSkills = testEffect( node, ), ) +const kit = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node)) + +const toolCtx: Tool.Context = { + sessionID: SessionID.make("ses_test-plugin-skill"), + messageID: MessageID.make("msg_test-plugin-skill"), + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, +} async function createGlobalSkill(homeDir: string) { const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill") @@ -566,4 +582,246 @@ description: A skill in the .opencode/skills directory. { git: true }, ), ) + it.live("discovers skills from config.skills.paths", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const root = path.join(dir, "config-skills") + yield* Effect.promise(() => + Bun.write( + path.join(root, "config-skill", "SKILL.md"), + `--- +name: config-skill +description: A skill registered via config.skills.paths. +--- + +# Config Skill +`, + ), + ) + yield* Effect.promise(() => + Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: { paths: [root] }, + }), + ), + ) + const skill = yield* Skill.Service + const item = (yield* skill.all()).find((x) => x.name === "config-skill") + expect(item).toBeDefined() + expect(item!.description).toBe("A skill registered via config.skills.paths.") + }), + { git: true }, + ), + ) + + it.live("returns skill from config.skills.paths", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const root = path.join(dir, "config-skills") + yield* Effect.promise(() => + Bun.write( + path.join(root, "get-test-skill", "SKILL.md"), + `--- +name: get-test-skill +description: Skill for get() test. +--- + +# Get Test Skill +`, + ), + ) + yield* Effect.promise(() => + Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: { paths: [root] }, + }), + ), + ) + const skill = yield* Skill.Service + const item = yield* skill.get("get-test-skill") + expect(item).toBeDefined() + expect(item!.name).toBe("get-test-skill") + expect(item!.description).toBe("Skill for get() test.") + }), + { git: true }, + ), + ) + + it.live("includes config.skills.paths directories in dirs", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const root = path.join(dir, "config-skills") + const item = path.join(root, "dirs-test-skill") + yield* Effect.promise(() => + Bun.write( + path.join(item, "SKILL.md"), + `--- +name: dirs-test-skill +description: Skill for dirs() test. +--- + +# Dirs Test Skill +`, + ), + ) + yield* Effect.promise(() => + Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: { paths: [root] }, + }), + ), + ) + const skill = yield* Skill.Service + expect(yield* skill.dirs()).toContain(item) + }), + { git: true }, + ), + ) + + it.live("returns undefined for missing skills", () => + provideTmpdirInstance( + () => + Effect.gen(function* () { + const skill = yield* Skill.Service + expect(yield* skill.get("nonexistent-skill")).toBeUndefined() + }), + { git: true }, + ), + ) + + it.live("keeps opencode and claude skill loading with config.skills.paths", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const root = path.join(dir, "config-skills") + yield* Effect.promise(() => + Promise.all([ + Bun.write( + path.join(dir, ".opencode", "skill", "opencode-skill", "SKILL.md"), + `--- +name: opencode-skill +description: Skill in .opencode/skill directory. +--- + +# OpenCode Skill +`, + ), + Bun.write( + path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"), + `--- +name: claude-skill +description: Skill in .claude/skills directory. +--- + +# Claude Skill +`, + ), + Bun.write( + path.join(root, "config-skill", "SKILL.md"), + `--- +name: config-skill +description: Skill in config.skills.paths. +--- + +# Config Skill +`, + ), + Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: { paths: [root] }, + }), + ), + ]), + ) + const skill = yield* Skill.Service + const list = (yield* skill.all()).filter((x) => x.location !== "") + expect(list.length).toBe(3) + expect(list.find((x) => x.name === "opencode-skill")).toBeDefined() + expect(list.find((x) => x.name === "claude-skill")).toBeDefined() + expect(list.find((x) => x.name === "config-skill")).toBeDefined() + }), + { git: true }, + ), + ) + + kit.live( + "PluginInput.skills works in plugin tool execute after config hooks populate skills.paths", + () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const plugin = path.join(dir, ".opencode", "plugin") + yield* Effect.promise(() => fs.mkdir(plugin, { recursive: true })) + yield* Effect.promise(() => + Bun.write( + path.join(dir, "plugin-config-skills", "plugin-config-skill", "SKILL.md"), + `--- +name: plugin-config-skill +description: A skill registered by a plugin config hook. +--- + +# Plugin Config Skill +`, + ), + ) + yield* Effect.promise(() => + Bun.write( + path.join(plugin, "plugin-skill-check.ts"), + [ + 'import { tool } from "@opencode-ai/plugin"', + "", + "export default async (input) => ({", + " config: async (config) => {", + " config.skills ??= {}", + " config.skills.paths ??= []", + ' config.skills.paths.push("./plugin-config-skills")', + " },", + " tool: {", + ' "plugin-skill-check": tool({', + ' description: "Checks PluginInput.skills access",', + " args: {},", + " execute: async () => {", + ' const skill = await input.skills.get("plugin-config-skill")', + " const dirs = await input.skills.dirs()", + " const all = await input.skills.all()", + ' return [skill?.name ?? "", skill?.description ?? "", String(all.length), ...dirs].join("\\n")', + " },", + " }),", + " },", + "})", + "", + ].join("\n"), + ), + ) + const list = yield* ToolRegistry.Service.use((svc) => + svc.tools({ + providerID: ProviderID.opencode, + modelID: ModelID.make("gpt-5"), + agent: { name: "build", mode: "primary", permission: [], options: {} }, + }), + ) + const tool = list.find((item) => item.id === "plugin-skill-check") + expect(tool).toBeDefined() + const out = yield* tool!.execute({}, toolCtx) + const lines = out.output.split("\n") + expect(lines[0]).toBe("plugin-config-skill") + expect(lines[1]).toBe("A skill registered by a plugin config hook.") + expect(Number(lines[2])).toBeGreaterThanOrEqual(1) + expect(lines).toContain(path.join(dir, "plugin-config-skills", "plugin-config-skill")) + }), + { git: true }, + ), + 30000, + ) }) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 6156477be216..df1d06602c26 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -53,6 +53,13 @@ export type WorkspaceAdapter = { target(config: WorkspaceInfo): WorkspaceTarget | Promise } +export type SkillInfo = { + name: string + description: string + location: string + content: string +} + export type PluginInput = { client: ReturnType project: Project @@ -63,6 +70,11 @@ export type PluginInput = { } serverUrl: URL $: BunShell + skills: { + all(): Promise + get(name: string): Promise + dirs(): Promise + } } export type PluginOptions = Record