feat: add opencode-skill-loader with 4-source priority system (#331)
* feat: add opencode-skill-loader with 4-source priority system
- Create new opencode-skill-loader feature module independent from Claude Code
- Support 4 source paths with priority: opencode-project > project > opencode > user
- .opencode/skill/ (opencode-project)
- .claude/skills/ (project)
- ~/.config/opencode/skill/ (opencode)
- ~/.claude/skills/ (user)
- Support both SKILL.md and {SKILLNAME}.md file patterns
- Maintain path awareness for file references (@path syntax)
* feat: integrate opencode-skill-loader into main plugin
- Import and use new skill loader functions
- Load skills from all 4 sources and merge into config.command
- Also merge pluginComponents.skills (previously loaded but never used)
* feat: add skill discovery to slashcommand tool
- Import and use discoverAllSkills from opencode-skill-loader
- Display skills alongside commands in tool description and execution
- Update formatCommandList to handle combined commands and skills
* refactor: remove old claude-code-skill-loader
- Delete src/features/claude-code-skill-loader/ directory (was never integrated into main plugin)
- Update plugin loader import to use new opencode-skill-loader types
* docs: update AGENTS.md for new skill loader
- Update structure to show opencode-skill-loader instead of claude-code-skill-loader
- Update Skills priority order to include all 4 sources
---------
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
@@ -19,7 +19,7 @@ features/
|
|||||||
│ └── env-expander.ts # ${VAR} expansion
|
│ └── env-expander.ts # ${VAR} expansion
|
||||||
├── claude-code-plugin-loader/ # Load external plugins from installed_plugins.json
|
├── claude-code-plugin-loader/ # Load external plugins from installed_plugins.json
|
||||||
├── claude-code-session-state/ # Session state persistence
|
├── claude-code-session-state/ # Session state persistence
|
||||||
├── claude-code-skill-loader/ # Load skills from ~/.claude/skills/*/SKILL.md
|
├── opencode-skill-loader/ # Load skills from OpenCode and Claude paths
|
||||||
└── hook-message-injector/ # Inject messages into conversation
|
└── hook-message-injector/ # Inject messages into conversation
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Each loader reads from multiple directories (highest priority first):
|
|||||||
| Loader | Priority Order |
|
| Loader | Priority Order |
|
||||||
|--------|---------------|
|
|--------|---------------|
|
||||||
| Commands | `.opencode/command/` > `~/.config/opencode/command/` > `.claude/commands/` > `~/.claude/commands/` |
|
| Commands | `.opencode/command/` > `~/.config/opencode/command/` > `.claude/commands/` > `~/.claude/commands/` |
|
||||||
| Skills | `.claude/skills/` > `~/.claude/skills/` |
|
| Skills | `.opencode/skill/` > `~/.config/opencode/skill/` > `.claude/skills/` > `~/.claude/skills/` |
|
||||||
| Agents | `.claude/agents/` > `~/.claude/agents/` |
|
| Agents | `.claude/agents/` > `~/.claude/agents/` |
|
||||||
| MCPs | `.claude/.mcp.json` > `.mcp.json` > `~/.claude/.mcp.json` |
|
| MCPs | `.claude/.mcp.json` > `.mcp.json` > `~/.claude/.mcp.json` |
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { log } from "../../shared/logger"
|
|||||||
import { expandEnvVarsInObject } from "../claude-code-mcp-loader/env-expander"
|
import { expandEnvVarsInObject } from "../claude-code-mcp-loader/env-expander"
|
||||||
import { transformMcpServer } from "../claude-code-mcp-loader/transformer"
|
import { transformMcpServer } from "../claude-code-mcp-loader/transformer"
|
||||||
import type { CommandDefinition, CommandFrontmatter } from "../claude-code-command-loader/types"
|
import type { CommandDefinition, CommandFrontmatter } from "../claude-code-command-loader/types"
|
||||||
import type { SkillMetadata } from "../claude-code-skill-loader/types"
|
import type { SkillMetadata } from "../opencode-skill-loader/types"
|
||||||
import type { AgentFrontmatter } from "../claude-code-agent-loader/types"
|
import type { AgentFrontmatter } from "../claude-code-agent-loader/types"
|
||||||
import type { ClaudeCodeMcpConfig, McpServerConfig } from "../claude-code-mcp-loader/types"
|
import type { ClaudeCodeMcpConfig, McpServerConfig } from "../claude-code-mcp-loader/types"
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
|
||||||
import { join } from "path"
|
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
|
||||||
import { resolveSymlink } from "../../shared/file-utils"
|
|
||||||
import { getClaudeConfigDir } from "../../shared"
|
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
|
||||||
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./types"
|
|
||||||
|
|
||||||
function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkillAsCommand[] {
|
|
||||||
if (!existsSync(skillsDir)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = readdirSync(skillsDir, { withFileTypes: true })
|
|
||||||
const skills: LoadedSkillAsCommand[] = []
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.name.startsWith(".")) continue
|
|
||||||
|
|
||||||
const skillPath = join(skillsDir, entry.name)
|
|
||||||
|
|
||||||
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
||||||
|
|
||||||
const resolvedPath = resolveSymlink(skillPath)
|
|
||||||
|
|
||||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
|
||||||
if (!existsSync(skillMdPath)) continue
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = readFileSync(skillMdPath, "utf-8")
|
|
||||||
const { data, body } = parseFrontmatter<SkillMetadata>(content)
|
|
||||||
|
|
||||||
const skillName = data.name || entry.name
|
|
||||||
const originalDescription = data.description || ""
|
|
||||||
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
|
||||||
|
|
||||||
const wrappedTemplate = `<skill-instruction>
|
|
||||||
Base directory for this skill: ${resolvedPath}/
|
|
||||||
File references (@path) in this skill are relative to this directory.
|
|
||||||
|
|
||||||
${body.trim()}
|
|
||||||
</skill-instruction>
|
|
||||||
|
|
||||||
<user-request>
|
|
||||||
$ARGUMENTS
|
|
||||||
</user-request>`
|
|
||||||
|
|
||||||
const definition: CommandDefinition = {
|
|
||||||
name: skillName,
|
|
||||||
description: formattedDescription,
|
|
||||||
template: wrappedTemplate,
|
|
||||||
model: sanitizeModelField(data.model),
|
|
||||||
}
|
|
||||||
|
|
||||||
skills.push({
|
|
||||||
name: skillName,
|
|
||||||
path: resolvedPath,
|
|
||||||
definition,
|
|
||||||
scope,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return skills
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadUserSkillsAsCommands(): Record<string, CommandDefinition> {
|
|
||||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
|
||||||
const skills = loadSkillsFromDir(userSkillsDir, "user")
|
|
||||||
return skills.reduce((acc, skill) => {
|
|
||||||
acc[skill.name] = skill.definition
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, CommandDefinition>)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadProjectSkillsAsCommands(): Record<string, CommandDefinition> {
|
|
||||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
|
||||||
const skills = loadSkillsFromDir(projectSkillsDir, "project")
|
|
||||||
return skills.reduce((acc, skill) => {
|
|
||||||
acc[skill.name] = skill.definition
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, CommandDefinition>)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
|
||||||
|
|
||||||
export type SkillScope = "user" | "project"
|
|
||||||
|
|
||||||
export interface SkillMetadata {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
model?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoadedSkillAsCommand {
|
|
||||||
name: string
|
|
||||||
path: string
|
|
||||||
definition: CommandDefinition
|
|
||||||
scope: SkillScope
|
|
||||||
}
|
|
||||||
179
src/features/opencode-skill-loader/loader.ts
Normal file
179
src/features/opencode-skill-loader/loader.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
|
import { join, basename, dirname } from "path"
|
||||||
|
import { homedir } from "os"
|
||||||
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
|
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
|
||||||
|
import { getClaudeConfigDir } from "../../shared"
|
||||||
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
|
import type { SkillScope, SkillMetadata, LoadedSkill } from "./types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a skill from a markdown file path.
|
||||||
|
*
|
||||||
|
* @param skillPath - Path to the skill file (SKILL.md or {name}.md)
|
||||||
|
* @param resolvedPath - Directory for file reference resolution (@path references)
|
||||||
|
* @param defaultName - Fallback name if not specified in frontmatter
|
||||||
|
* @param scope - Source scope for priority ordering
|
||||||
|
*/
|
||||||
|
function loadSkillFromPath(
|
||||||
|
skillPath: string,
|
||||||
|
resolvedPath: string,
|
||||||
|
defaultName: string,
|
||||||
|
scope: SkillScope
|
||||||
|
): LoadedSkill | null {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(skillPath, "utf-8")
|
||||||
|
const { data, body } = parseFrontmatter<SkillMetadata>(content)
|
||||||
|
|
||||||
|
const skillName = data.name || defaultName
|
||||||
|
const originalDescription = data.description || ""
|
||||||
|
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||||
|
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||||
|
|
||||||
|
const wrappedTemplate = `<skill-instruction>
|
||||||
|
Base directory for this skill: ${resolvedPath}/
|
||||||
|
File references (@path) in this skill are relative to this directory.
|
||||||
|
|
||||||
|
${body.trim()}
|
||||||
|
</skill-instruction>
|
||||||
|
|
||||||
|
<user-request>
|
||||||
|
$ARGUMENTS
|
||||||
|
</user-request>`
|
||||||
|
|
||||||
|
const definition: CommandDefinition = {
|
||||||
|
name: skillName,
|
||||||
|
description: formattedDescription,
|
||||||
|
template: wrappedTemplate,
|
||||||
|
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||||
|
agent: data.agent,
|
||||||
|
subtask: data.subtask,
|
||||||
|
argumentHint: data["argument-hint"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: skillName,
|
||||||
|
path: skillPath,
|
||||||
|
resolvedPath,
|
||||||
|
definition,
|
||||||
|
scope,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skills from a directory, supporting BOTH patterns:
|
||||||
|
* - Directory with SKILL.md: skill-name/SKILL.md
|
||||||
|
* - Directory with {SKILLNAME}.md: skill-name/{SKILLNAME}.md
|
||||||
|
* - Direct markdown file: skill-name.md
|
||||||
|
*/
|
||||||
|
function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkill[] {
|
||||||
|
if (!existsSync(skillsDir)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = readdirSync(skillsDir, { withFileTypes: true })
|
||||||
|
const skills: LoadedSkill[] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name.startsWith(".")) continue
|
||||||
|
|
||||||
|
const entryPath = join(skillsDir, entry.name)
|
||||||
|
|
||||||
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
||||||
|
const resolvedPath = resolveSymlink(entryPath)
|
||||||
|
const dirName = entry.name
|
||||||
|
|
||||||
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
|
if (existsSync(skillMdPath)) {
|
||||||
|
const skill = loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope)
|
||||||
|
if (skill) skills.push(skill)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||||
|
if (existsSync(namedSkillMdPath)) {
|
||||||
|
const skill = loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope)
|
||||||
|
if (skill) skills.push(skill)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMarkdownFile(entry)) {
|
||||||
|
const skillName = basename(entry.name, ".md")
|
||||||
|
const skill = loadSkillFromPath(entryPath, skillsDir, skillName, scope)
|
||||||
|
if (skill) skills.push(skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills
|
||||||
|
}
|
||||||
|
|
||||||
|
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
||||||
|
const result: Record<string, CommandDefinition> = {}
|
||||||
|
for (const skill of skills) {
|
||||||
|
result[skill.name] = skill.definition
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skills from Claude Code user directory (~/.claude/skills/)
|
||||||
|
*/
|
||||||
|
export function loadUserSkills(): Record<string, CommandDefinition> {
|
||||||
|
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||||
|
const skills = loadSkillsFromDir(userSkillsDir, "user")
|
||||||
|
return skillsToRecord(skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skills from Claude Code project directory (.claude/skills/)
|
||||||
|
*/
|
||||||
|
export function loadProjectSkills(): Record<string, CommandDefinition> {
|
||||||
|
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||||
|
const skills = loadSkillsFromDir(projectSkillsDir, "project")
|
||||||
|
return skillsToRecord(skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skills from OpenCode global directory (~/.config/opencode/skill/)
|
||||||
|
*/
|
||||||
|
export function loadOpencodeGlobalSkills(): Record<string, CommandDefinition> {
|
||||||
|
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||||
|
const skills = loadSkillsFromDir(opencodeSkillsDir, "opencode")
|
||||||
|
return skillsToRecord(skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skills from OpenCode project directory (.opencode/skill/)
|
||||||
|
*/
|
||||||
|
export function loadOpencodeProjectSkills(): Record<string, CommandDefinition> {
|
||||||
|
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||||
|
const skills = loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||||
|
return skillsToRecord(skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all skills from all sources with priority ordering.
|
||||||
|
* Priority order: opencode-project > project > opencode > user
|
||||||
|
*
|
||||||
|
* @returns Array of LoadedSkill objects for use in slashcommand discovery
|
||||||
|
*/
|
||||||
|
export function discoverAllSkills(): LoadedSkill[] {
|
||||||
|
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||||
|
const projectDir = join(process.cwd(), ".claude", "skills")
|
||||||
|
const opencodeGlobalDir = join(homedir(), ".config", "opencode", "skill")
|
||||||
|
const userDir = join(getClaudeConfigDir(), "skills")
|
||||||
|
|
||||||
|
const opencodeProjectSkills = loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||||
|
const projectSkills = loadSkillsFromDir(projectDir, "project")
|
||||||
|
const opencodeGlobalSkills = loadSkillsFromDir(opencodeGlobalDir, "opencode")
|
||||||
|
const userSkills = loadSkillsFromDir(userDir, "user")
|
||||||
|
|
||||||
|
return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]
|
||||||
|
}
|
||||||
20
src/features/opencode-skill-loader/types.ts
Normal file
20
src/features/opencode-skill-loader/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
|
|
||||||
|
export type SkillScope = "user" | "project" | "opencode" | "opencode-project"
|
||||||
|
|
||||||
|
export interface SkillMetadata {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
model?: string
|
||||||
|
"argument-hint"?: string
|
||||||
|
agent?: string
|
||||||
|
subtask?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadedSkill {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
resolvedPath: string
|
||||||
|
definition: CommandDefinition
|
||||||
|
scope: SkillScope
|
||||||
|
}
|
||||||
17
src/index.ts
17
src/index.ts
@@ -33,6 +33,12 @@ import {
|
|||||||
loadOpencodeProjectCommands,
|
loadOpencodeProjectCommands,
|
||||||
} from "./features/claude-code-command-loader";
|
} from "./features/claude-code-command-loader";
|
||||||
import { loadBuiltinCommands } from "./features/builtin-commands";
|
import { loadBuiltinCommands } from "./features/builtin-commands";
|
||||||
|
import {
|
||||||
|
loadUserSkills,
|
||||||
|
loadProjectSkills,
|
||||||
|
loadOpencodeGlobalSkills,
|
||||||
|
loadOpencodeProjectSkills,
|
||||||
|
} from "./features/opencode-skill-loader";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
loadUserAgents,
|
loadUserAgents,
|
||||||
@@ -523,14 +529,25 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const systemCommands = config.command ?? {};
|
const systemCommands = config.command ?? {};
|
||||||
const projectCommands = (pluginConfig.claude_code?.commands ?? true) ? loadProjectCommands() : {};
|
const projectCommands = (pluginConfig.claude_code?.commands ?? true) ? loadProjectCommands() : {};
|
||||||
const opencodeProjectCommands = loadOpencodeProjectCommands();
|
const opencodeProjectCommands = loadOpencodeProjectCommands();
|
||||||
|
|
||||||
|
const userSkills = (pluginConfig.claude_code?.skills ?? true) ? loadUserSkills() : {};
|
||||||
|
const projectSkills = (pluginConfig.claude_code?.skills ?? true) ? loadProjectSkills() : {};
|
||||||
|
const opencodeGlobalSkills = loadOpencodeGlobalSkills();
|
||||||
|
const opencodeProjectSkills = loadOpencodeProjectSkills();
|
||||||
|
|
||||||
config.command = {
|
config.command = {
|
||||||
...builtinCommands,
|
...builtinCommands,
|
||||||
...userCommands,
|
...userCommands,
|
||||||
|
...userSkills,
|
||||||
...opencodeGlobalCommands,
|
...opencodeGlobalCommands,
|
||||||
|
...opencodeGlobalSkills,
|
||||||
...systemCommands,
|
...systemCommands,
|
||||||
...projectCommands,
|
...projectCommands,
|
||||||
|
...projectSkills,
|
||||||
...opencodeProjectCommands,
|
...opencodeProjectCommands,
|
||||||
|
...opencodeProjectSkills,
|
||||||
...pluginComponents.commands,
|
...pluginComponents.commands,
|
||||||
|
...pluginComponents.skills,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join, basename, dirname } from "path"
|
|||||||
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
|
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import { getClaudeConfigDir } from "../../shared"
|
import { getClaudeConfigDir } from "../../shared"
|
||||||
|
import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
|
||||||
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
|
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
|
||||||
|
|
||||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||||
@@ -64,8 +65,30 @@ function discoverCommandsSync(): CommandInfo[] {
|
|||||||
return [...opencodeProjectCommands, ...projectCommands, ...opencodeGlobalCommands, ...userCommands]
|
return [...opencodeProjectCommands, ...projectCommands, ...opencodeGlobalCommands, ...userCommands]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||||
|
return {
|
||||||
|
name: skill.name,
|
||||||
|
path: skill.path,
|
||||||
|
metadata: {
|
||||||
|
name: skill.name,
|
||||||
|
description: skill.definition.description || "",
|
||||||
|
argumentHint: skill.definition.argumentHint,
|
||||||
|
model: skill.definition.model,
|
||||||
|
agent: skill.definition.agent,
|
||||||
|
subtask: skill.definition.subtask,
|
||||||
|
},
|
||||||
|
content: skill.definition.template,
|
||||||
|
scope: skill.scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const availableCommands = discoverCommandsSync()
|
const availableCommands = discoverCommandsSync()
|
||||||
const commandListForDescription = availableCommands
|
const availableSkills = discoverAllSkills()
|
||||||
|
const availableItems = [
|
||||||
|
...availableCommands,
|
||||||
|
...availableSkills.map(skillToCommandInfo),
|
||||||
|
]
|
||||||
|
const commandListForDescription = availableItems
|
||||||
.map((cmd) => {
|
.map((cmd) => {
|
||||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||||
return `- /${cmd.name}${hint}: ${cmd.metadata.description} (${cmd.scope})`
|
return `- /${cmd.name}${hint}: ${cmd.metadata.description} (${cmd.scope})`
|
||||||
@@ -109,21 +132,21 @@ async function formatLoadedCommand(cmd: CommandInfo): Promise<string> {
|
|||||||
return sections.join("\n")
|
return sections.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCommandList(commands: CommandInfo[]): string {
|
function formatCommandList(items: CommandInfo[]): string {
|
||||||
if (commands.length === 0) {
|
if (items.length === 0) {
|
||||||
return "No commands found."
|
return "No commands or skills found."
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = ["# Available Commands\n"]
|
const lines = ["# Available Commands & Skills\n"]
|
||||||
|
|
||||||
for (const cmd of commands) {
|
for (const cmd of items) {
|
||||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||||
lines.push(
|
lines.push(
|
||||||
`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`
|
`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(`\n**Total**: ${commands.length} commands`)
|
lines.push(`\n**Total**: ${items.length} items`)
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +171,13 @@ Commands are loaded from (priority order, highest wins):
|
|||||||
- ~/.config/opencode/command/ (opencode - OpenCode global commands)
|
- ~/.config/opencode/command/ (opencode - OpenCode global commands)
|
||||||
- $CLAUDE_CONFIG_DIR/commands/ or ~/.claude/commands/ (user - Claude Code global commands)
|
- $CLAUDE_CONFIG_DIR/commands/ or ~/.claude/commands/ (user - Claude Code global commands)
|
||||||
|
|
||||||
Each command is a markdown file with:
|
Skills are loaded from (priority order, highest wins):
|
||||||
|
- .opencode/skill/ (opencode-project - OpenCode project-specific skills)
|
||||||
|
- ./.claude/skills/ (project - Claude Code project-specific skills)
|
||||||
|
- ~/.config/opencode/skill/ (opencode - OpenCode global skills)
|
||||||
|
- $CLAUDE_CONFIG_DIR/skills/ or ~/.claude/skills/ (user - Claude Code global skills)
|
||||||
|
|
||||||
|
Each command/skill is a markdown file with:
|
||||||
- YAML frontmatter: description, argument-hint, model, agent, subtask (optional)
|
- YAML frontmatter: description, argument-hint, model, agent, subtask (optional)
|
||||||
- Markdown body: The command instructions/prompt
|
- Markdown body: The command instructions/prompt
|
||||||
- File references: @path/to/file (relative to command file location)
|
- File references: @path/to/file (relative to command file location)
|
||||||
@@ -167,14 +196,19 @@ ${commandListForDescription}`,
|
|||||||
|
|
||||||
async execute(args) {
|
async execute(args) {
|
||||||
const commands = discoverCommandsSync()
|
const commands = discoverCommandsSync()
|
||||||
|
const skills = discoverAllSkills()
|
||||||
|
const allItems = [
|
||||||
|
...commands,
|
||||||
|
...skills.map(skillToCommandInfo),
|
||||||
|
]
|
||||||
|
|
||||||
if (!args.command) {
|
if (!args.command) {
|
||||||
return formatCommandList(commands) + "\n\nProvide a command name to execute."
|
return formatCommandList(allItems) + "\n\nProvide a command or skill name to execute."
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdName = args.command.replace(/^\//, "")
|
const cmdName = args.command.replace(/^\//, "")
|
||||||
|
|
||||||
const exactMatch = commands.find(
|
const exactMatch = allItems.find(
|
||||||
(cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase()
|
(cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,7 +216,7 @@ ${commandListForDescription}`,
|
|||||||
return await formatLoadedCommand(exactMatch)
|
return await formatLoadedCommand(exactMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const partialMatches = commands.filter((cmd) =>
|
const partialMatches = allItems.filter((cmd) =>
|
||||||
cmd.name.toLowerCase().includes(cmdName.toLowerCase())
|
cmd.name.toLowerCase().includes(cmdName.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -190,14 +224,14 @@ ${commandListForDescription}`,
|
|||||||
const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ")
|
const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ")
|
||||||
return (
|
return (
|
||||||
`No exact match for "/${cmdName}". Did you mean: ${matchList}?\n\n` +
|
`No exact match for "/${cmdName}". Did you mean: ${matchList}?\n\n` +
|
||||||
formatCommandList(commands)
|
formatCommandList(allItems)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`Command "/${cmdName}" not found.\n\n` +
|
`Command or skill "/${cmdName}" not found.\n\n` +
|
||||||
formatCommandList(commands) +
|
formatCommandList(allItems) +
|
||||||
"\n\nTry a different command name."
|
"\n\nTry a different name."
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user