Documentation
¶
Overview ¶
Package skills reads and manages the agent skills installed on this machine.
A skill is a directory holding a SKILL.md whose YAML frontmatter names and describes it. Every agent that supports skills uses that same shape, and none of them expose skills over ACP — so opentree discovers them by walking directories, and can add, copy, and remove them the same way.
Index ¶
- func Bridge(repoRoot string) ([]string, error)
- func Clone(url string, dirs ...string) error
- func CloneName(url string) string
- func CopyTo(s Skill, dirs ...string) error
- func Delete(list ...Skill) error
- func ExpandUserDir(path string) string
- func Install(ctx context.Context, e Entry, dirs ...string) error
- func Link(repoRoot, worktreePath string) ([]string, error)
- func Missing(repoRoot, worktreePath string) []string
- func OverrideFile(spec config.SkillsSpec, repoRoot, skill string) string
- func Probe(ctx context.Context, agent config.PredefinedAgent, cwd string) (map[string]bool, error)
- func SetDisabled(spec config.SkillsSpec, repoRoot, skill string, state State) (string, error)
- func SetOverride(spec config.SkillsSpec, repoRoot, skill string, state State) (string, error)
- func SetState(spec config.SkillsSpec, repoRoot, skill string, state State) (string, error)
- func Update(ctx context.Context, dir string) (bool, error)
- type Entry
- type Scope
- type Skill
- type State
- type Tree
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Bridge ¶
Bridge points every agent's repository skills tree at the one this repository actually has, and reports the trees it created.
Which agent reads which repository tree is not symmetric. opencode reads .claude/skills and .agents/skills as well as its own; Claude Code reads only .claude/skills. So a project skill kept under .opencode/skills is one the list shows, that opentree links into every worktree, and that Claude Code working there still cannot use.
A relative link, so it means the same thing from a worktree and survives being committed: a repository that tracks its skills can track the bridge with them, and then opentree has nothing left to do.
Not automatic, unlike Link. A worktree is opentree's own directory and linking inside it is housekeeping; the repository root is the user's, and a new directory there is theirs to ask for.
func Clone ¶
Clone installs a skill from a git repository into each of the given trees — the other way a skill arrives, alongside copying one that is already here.
The .git directory is kept, so `git -C <dir> pull` updates the skill later. opentree records no provenance of its own and there is no registry to re-fetch from; the clone is the only thing that remembers where the skill came from.
Cloned once and copied into the rest, .git and all, so each tree's copy stays independently pullable without asking the remote the same question once per agent.
func CloneName ¶
cloneName is the directory a URL clones into: its last path element, without the .git suffix. Anything hidden or with no name at all is refused rather than guessed at — the name becomes a directory under the user's skills.
func CopyTo ¶
CopyTo installs a copy of a skill into each of the given trees — the way a skill written for one agent reaches another, since they all read the same format.
A tree that refuses the copy does not cost the others theirs: the errors are collected and every remaining tree is still tried. Nothing here is a step in one operation; each is its own agent getting its own copy.
func Delete ¶
Delete removes each skill's directory.
Several at once because one skill commonly sits in several trees, and taking it out of two of the three is an ordinary thing to want. A directory that refuses does not cost the others theirs.
func ExpandUserDir ¶
ExpandUserDir resolves a registry skills path against the user's home.
XDG_CONFIG_HOME is honoured for the ~/.config prefix because opencode reads it: a user who moved their config would otherwise be shown an empty list and told they have no skills.
func Install ¶
Install downloads one entry into its own directory under each of the given trees.
Fetched once and copied into the rest: agents read each other's directories unevenly, so covering all of them takes more than one tree — and the same artifact pulled once per tree is the same bytes over the wire as many times as there are agents. Each copy carries the source file with it, so all of them can be updated later.
func Link ¶
Link points a worktree at the repository's skills and reports which trees it linked. Doing nothing is the common, correct outcome: a repository with no skills has nothing to share, and one that commits them has already had them checked out by git.
A symlink rather than a copy, because a copy starts drifting the moment either side is edited, and a project skill is one thing rather than one per branch. Editing it from inside a worktree edits the repository's copy, which is what a shared skill means.
func Missing ¶
Missing reports the repository skill trees a worktree cannot see — what the list warns about and what relinking would repair.
Stat rather than Lstat here, so a link left dangling by a deleted or renamed skills directory counts as missing. It is a link that resolves to nothing, and the agent behind it sees no skills at all.
func OverrideFile ¶
func OverrideFile(spec config.SkillsSpec, repoRoot, skill string) string
OverrideFile is where a change to a skill's state should be written: the file already carrying an override for it, and otherwise the first one the agent reads.
Preferring the file that already holds the override is what makes the write take effect. Writing "on" into a higher-precedence file while a lower one still says "off" only works if the agent layers them that way, and the one thing observation proves is that an override written where the others live is honoured.
func Probe ¶
Probe starts the agent, opens a session, and reports the commands it advertises, keyed by name.
It costs a real session in the agent's own storage — commands are a session update, so there is no way to be told without starting one. An empty conversation is cheap and the agent's own session list is full of them; the alternative is not being able to check at all.
func SetDisabled ¶
SetDisabled switches a skill off for an agent that keeps a list of the disabled rather than a map of states, or takes it off that list again.
A list carries less than a map: a name is on it or it is not, so off and default are the only two things it can say. Anything else is refused rather than approximated into the nearest thing the list can hold.
Turning a skill back on clears it from every file that names it, not only from the one an addition would go to. The files union when read — a name missing from one says nothing about the others — so clearing the repository's while the user's still lists it would report a skill as on that the agent goes on ignoring.
func SetOverride ¶
SetOverride records a skill's state in the agent's settings, or clears the override when state is empty.
Clearing rather than writing "on" is the way back to the default, because "on" is not always the default: a skill whose own frontmatter asks not to be model-invoked would be quietly promoted to fully automatic by an explicit "on", which is not what turning it back on means.
Only the overrides object is rewritten. The rest of the file keeps its bytes, including the order of keys around it — a settings file is hand-maintained and frequently version-controlled, and reformatting all of it to change one entry is not a change the user asked for.
func SetState ¶
SetState switches a skill off for one agent, or clears what switched it off, by whichever mechanism that agent has. Callers should not have to know which shape a given agent keeps its answer in.
func Update ¶
Update re-checks an installed skill against the site that published it, and replaces it when the published bytes have changed. It reports whether anything changed.
The digest is what makes this cheap: an unchanged skill is settled by the index alone, with nothing downloaded and nothing on disk touched. That is the check a clone cannot do — git has to fetch before it can tell.
Types ¶
type Entry ¶
type Entry struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
URL string `json:"url"`
Digest string `json:"digest"`
// contains filtered or unexported fields
}
An Entry is one skill a site offers. Description is carried so the picker can show what a skill is for before it is on disk — the same line the agent will later match against.
type Skill ¶
type Skill struct {
Name string // frontmatter name, falling back to the directory name
Description string
Dir string // the directory holding SKILL.md
// Agents are every registry agent that reads this skill's tree, in registry
// order. Usually more than one: agents auto-load each other's directories,
// so a skill installed once is commonly usable from all of them.
Agents []string
Scope Scope
// States is each agent's effective availability, keyed by agent name. An
// agent reading the same file can still have been told to ignore it, so
// this is per agent rather than per skill.
States map[string]State
// ManualOnly records the skill's own request not to be model-invoked,
// before any override. Kept apart from States so clearing an override
// returns the skill to what it asked for rather than to fully automatic.
ManualOnly bool
}
Skill is one SKILL.md and the directory holding it.
func Scan ¶
Scan finds every skill opentree can see, once each.
Trees overlap by symlink as well as by configuration: a skill installer may keep the real directory in one tree and link it into another, so the same SKILL.md is reachable by two paths. Those are one skill readable by both agents, not two — collapsing them on the resolved path is what keeps the list honest, and what leaves the "duplicate" tag meaning a genuine conflict between two different files.
The surviving entry is the resolved directory, so editing and deleting act on the real skill rather than on a link to it.
Ordering is by name then directory, so the listing is stable and two same-named skills sit together where the difference is visible.
type State ¶
type State string
State is how available a skill is to one agent. The values are Claude Code's own, taken from the schema its binary carries for the overrides map.
const ( // StateOn is the default: the agent loads it and may invoke it itself. StateOn State = "on" // StateNameOnly lists the skill without its description. StateNameOnly State = "name-only" // StateManualOnly loads the skill but leaves invoking it to the user. StateManualOnly State = "user-invocable-only" // StateOff means the agent does not load it at all. StateOff State = "off" )
type Tree ¶
type Tree struct {
Dir string
Scope Scope
Agents []string
// Deep searches the tree at any depth rather than listing it one level
// down. True only for a directory the user registered in their own config:
// that is a place to look, and the agent reading it searches it for
// **/SKILL.md. The standard trees are listed one level deep, because there
// a nested SKILL.md is a skill's own reference material rather than a
// second skill.
Deep bool
}
Tree is one skills directory and the agents that read it.
Directories are the unit rather than agents because the mapping is many-to-many — opencode loads Claude Code's global tree as its own — and scanning per agent would read the same directory twice and report one skill as two.