summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/registry.ts3
-rw-r--r--packages/opencode/src/tool/skill.ts162
-rw-r--r--packages/opencode/test/tool/skill.test.ts8
3 files changed, 90 insertions, 83 deletions
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index a24ddb28c..f6324b3d7 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -116,6 +116,7 @@ export namespace ToolRegistry {
const edit = yield* EditTool
const greptool = yield* GrepTool
const patchtool = yield* ApplyPatchTool
+ const skilltool = yield* SkillTool
const state = yield* InstanceState.make<State>(
Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -185,7 +186,7 @@ export namespace ToolRegistry {
todo: Tool.init(todo),
search: Tool.init(websearch),
code: Tool.init(codesearch),
- skill: Tool.init(SkillTool),
+ skill: Tool.init(skilltool),
patch: Tool.init(patchtool),
question: Tool.init(question),
lsp: Tool.init(lsptool),
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index e0777d00f..f53f4e2bc 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -1,99 +1,101 @@
import path from "path"
import { pathToFileURL } from "url"
import z from "zod"
+import { Effect } from "effect"
+import * as Stream from "effect/Stream"
import { Tool } from "./tool"
import { Skill } from "../skill"
import { Ripgrep } from "../file/ripgrep"
-import { iife } from "@/util/iife"
const Parameters = z.object({
name: z.string().describe("The name of the skill from available_skills"),
})
-export const SkillTool = Tool.define("skill", async () => {
- const list = await Skill.available()
+export const SkillTool = Tool.defineEffect(
+ "skill",
+ Effect.gen(function* () {
+ const skill = yield* Skill.Service
+ const rg = yield* Ripgrep.Service
- const description =
- list.length === 0
- ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
- : [
- "Load a specialized skill that provides domain-specific instructions and workflows.",
- "",
- "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
- "",
- "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
- "",
- 'Tool output includes a `<skill_content name="...">` block with the loaded content.',
- "",
- "The following skills provide specialized sets of instructions for particular tasks",
- "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
- "",
- Skill.fmt(list, { verbose: false }),
- ].join("\n")
+ return async () => {
+ const list = await Effect.runPromise(skill.available())
- return {
- description,
- parameters: Parameters,
- async execute(params: z.infer<typeof Parameters>, ctx) {
- const skill = await Skill.get(params.name)
+ const description =
+ list.length === 0
+ ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
+ : [
+ "Load a specialized skill that provides domain-specific instructions and workflows.",
+ "",
+ "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
+ "",
+ "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
+ "",
+ 'Tool output includes a `<skill_content name="...">` block with the loaded content.',
+ "",
+ "The following skills provide specialized sets of instructions for particular tasks",
+ "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
+ "",
+ Skill.fmt(list, { verbose: false }),
+ ].join("\n")
- if (!skill) {
- const available = await Skill.all().then((x) => x.map((skill) => skill.name).join(", "))
- throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
- }
+ return {
+ description,
+ parameters: Parameters,
+ execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
+ Effect.gen(function* () {
+ const info = yield* skill.get(params.name)
- await ctx.ask({
- permission: "skill",
- patterns: [params.name],
- always: [params.name],
- metadata: {},
- })
+ if (!info) {
+ const all = yield* skill.all()
+ const available = all.map((s) => s.name).join(", ")
+ throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
+ }
- const dir = path.dirname(skill.location)
- const base = pathToFileURL(dir).href
+ yield* Effect.promise(() =>
+ ctx.ask({
+ permission: "skill",
+ patterns: [params.name],
+ always: [params.name],
+ metadata: {},
+ }),
+ )
- const limit = 10
- const files = await iife(async () => {
- const arr = []
- for await (const file of Ripgrep.files({
- cwd: dir,
- follow: false,
- hidden: true,
- signal: ctx.abort,
- })) {
- if (file.includes("SKILL.md")) {
- continue
- }
- arr.push(path.resolve(dir, file))
- if (arr.length >= limit) {
- break
- }
- }
- return arr
- }).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))
+ const dir = path.dirname(info.location)
+ const base = pathToFileURL(dir).href
- return {
- title: `Loaded skill: ${skill.name}`,
- output: [
- `<skill_content name="${skill.name}">`,
- `# Skill: ${skill.name}`,
- "",
- skill.content.trim(),
- "",
- `Base directory for this skill: ${base}`,
- "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
- "Note: file list is sampled.",
- "",
- "<skill_files>",
- files,
- "</skill_files>",
- "</skill_content>",
- ].join("\n"),
- metadata: {
- name: skill.name,
- dir,
- },
+ const limit = 10
+ const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe(
+ Stream.filter((file) => !file.includes("SKILL.md")),
+ Stream.map((file) => path.resolve(dir, file)),
+ Stream.take(limit),
+ Stream.runCollect,
+ Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
+ )
+
+ return {
+ title: `Loaded skill: ${info.name}`,
+ output: [
+ `<skill_content name="${info.name}">`,
+ `# Skill: ${info.name}`,
+ "",
+ info.content.trim(),
+ "",
+ `Base directory for this skill: ${base}`,
+ "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
+ "Note: file list is sampled.",
+ "",
+ "<skill_files>",
+ files,
+ "</skill_files>",
+ "</skill_content>",
+ ].join("\n"),
+ metadata: {
+ name: info.name,
+ dir,
+ },
+ }
+ }).pipe(Effect.orDie, Effect.runPromise),
}
- },
- }
-})
+ }
+ }),
+)
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index ea9aeeaf9..1c97ee4af 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/packages/opencode/test/tool/skill.test.ts
@@ -1,4 +1,6 @@
-import { Effect } from "effect"
+import { Effect, Layer, ManagedRuntime } from "effect"
+import { Skill } from "../../src/skill"
+import { Ripgrep } from "../../src/file/ripgrep"
import { afterEach, describe, expect, test } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
@@ -148,7 +150,9 @@ Use this skill.
await Instance.provide({
directory: tmp.path,
fn: async () => {
- const tool = await SkillTool.init()
+ const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer))
+ const info = await runtime.runPromise(SkillTool)
+ const tool = await info.init()
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
...baseCtx,