export * as Skill from "./skill.js" import { makeLocationNode } from "@opencode/util/effect/app-node" import type { FSUtil } from "@opencode/util/fs-util" import path from "path" import { Context, Effect, Layer, Types } from "effect" import { Skill } from "@opencode/schema/skill" import { Bus } from "./bus.js" import { Permission } from "./permission.js" import { State } from "./state.js" export const DirectorySource = Skill.DirectorySource export type DirectorySource = Skill.DirectorySource export const UrlSource = Skill.UrlSource export type UrlSource = Skill.UrlSource export const EmbeddedSource = Skill.EmbeddedSource export type EmbeddedSource = Skill.EmbeddedSource export const Source = Skill.Source export type Source = Skill.Source export const Info = Skill.Info export type Info = Skill.Info export const ID = Skill.ID export type ID = Skill.ID export const Name = Skill.Name export type Name = Skill.Name export { Event } from "@opencode/schema/skill" export const available = (skills: ReadonlyArray, permissions: Permission.Ruleset) => skills.filter((skill) => Permission.evaluate("skill", skill.id, permissions).effect !== "deny") export const toModelOutput = (skill: Info, files: ReadonlyArray) => { const directory = path.dirname(skill.path) return [ ``, `# Skill: ${skill.name}`, "", skill.content.trim(), "", `Base directory for this skill: ${directory}`, "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", "Note: file list is sampled.", "", "", ...files.map((file) => `${file}`), "", "", ].join("\n") } export const prepare = Effect.fn("Skill.prepare")(function* (fs: FSUtil.Interface, skill: Info) { const directory = path.dirname(skill.path) const files = path.basename(skill.path) === "SKILL.md" ? (yield* fs.scan("**/*", { cwd: directory, absolute: true, include: "file", dot: true })) .filter((file) => path.basename(file) !== "SKILL.md") .toSorted() .slice(0, 10) : [] return { directory, output: toModelOutput(skill, files), } }) export type Data = { skills: Map> } export type Editor = { list: () => readonly Types.DeepMutable[] get: (id: string) => Types.DeepMutable | undefined add: (skill: Info) => void update: (id: string, update: (skill: Types.DeepMutable) => void) => void remove: (id: string) => void } export interface Interface extends State.Transformable { readonly get: (id: ID) => Effect.Effect readonly list: () => Effect.Effect } export class Service extends Context.Service()("@opencode/Skill") {} const layer = Layer.effect( Service, Effect.gen(function* () { const bus = yield* Bus.Service const state = State.create({ name: "skill", initial: () => ({ skills: new Map() }), editor: (editor) => ({ list: () => Array.from(editor.skills.values()), get: (id) => editor.skills.get(ID.make(id)), add: (skill) => { editor.skills.set(skill.id, { ...skill } as Types.DeepMutable) }, update: (id, update) => { const current = editor.skills.get(ID.make(id)) if (!current) return update(current) current.id = ID.make(id) }, remove: (id) => { editor.skills.delete(ID.make(id)) }, }), notify: () => bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid), }) return Service.of({ transform: state.transform, reload: state.reload, get: Effect.fn("Skill.get")(function* (id) { return state.get().skills.get(id) }), list: Effect.fn("Skill.list")(function* () { return Array.from(state.get().skills.values()) }), }) }), ) export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node], })