refactor(loaders): migrate to async-first pattern for commands and skills
- Remove all sync functions from command loader (async now default) - Remove sync load functions from skill loader (async now default) - Add resolveSymlinkAsync to file-utils.ts - Update all callers to use async versions: - config-handler.ts - index.ts - tools/slashcommand/tools.ts - tools/skill/tools.ts - hooks/auto-slash-command/executor.ts - loader.test.ts - All 607 tests pass, build succeeds Generated with assistance of 🤖 [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { existsSync, readdirSync, readFileSync, realpathSync, type Dirent } from "fs"
|
||||
import { promises as fsPromises } from "fs"
|
||||
import { promises as fs, type Dirent } from "fs"
|
||||
import { join, basename } from "path"
|
||||
import { homedir } from "os"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
@@ -8,19 +8,21 @@ import { getClaudeConfigDir } from "../../shared"
|
||||
import { log } from "../../shared/logger"
|
||||
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
||||
|
||||
function loadCommandsFromDir(
|
||||
async function loadCommandsFromDir(
|
||||
commandsDir: string,
|
||||
scope: CommandScope,
|
||||
visited: Set<string> = new Set(),
|
||||
prefix: string = ""
|
||||
): LoadedCommand[] {
|
||||
if (!existsSync(commandsDir)) {
|
||||
): Promise<LoadedCommand[]> {
|
||||
try {
|
||||
await fs.access(commandsDir)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
let realPath: string
|
||||
try {
|
||||
realPath = realpathSync(commandsDir)
|
||||
realPath = await fs.realpath(commandsDir)
|
||||
} catch (error) {
|
||||
log(`Failed to resolve command directory: ${commandsDir}`, error)
|
||||
return []
|
||||
@@ -33,7 +35,7 @@ function loadCommandsFromDir(
|
||||
|
||||
let entries: Dirent[]
|
||||
try {
|
||||
entries = readdirSync(commandsDir, { withFileTypes: true })
|
||||
entries = await fs.readdir(commandsDir, { withFileTypes: true })
|
||||
} catch (error) {
|
||||
log(`Failed to read command directory: ${commandsDir}`, error)
|
||||
return []
|
||||
@@ -46,7 +48,8 @@ function loadCommandsFromDir(
|
||||
if (entry.name.startsWith(".")) continue
|
||||
const subDirPath = join(commandsDir, entry.name)
|
||||
const subPrefix = prefix ? `${prefix}:${entry.name}` : entry.name
|
||||
commands.push(...loadCommandsFromDir(subDirPath, scope, visited, subPrefix))
|
||||
const subCommands = await loadCommandsFromDir(subDirPath, scope, visited, subPrefix)
|
||||
commands.push(...subCommands)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -57,7 +60,7 @@ function loadCommandsFromDir(
|
||||
const commandName = prefix ? `${prefix}:${baseCommandName}` : baseCommandName
|
||||
|
||||
try {
|
||||
const content = readFileSync(commandPath, "utf-8")
|
||||
const content = await fs.readFile(commandPath, "utf-8")
|
||||
const { data, body } = parseFrontmatter<CommandFrontmatter>(content)
|
||||
|
||||
const wrappedTemplate = `<command-instruction>
|
||||
@@ -106,154 +109,36 @@ function commandsToRecord(commands: LoadedCommand[]): Record<string, CommandDefi
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadUserCommands(): Record<string, CommandDefinition> {
|
||||
export async function loadUserCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const commands = loadCommandsFromDir(userCommandsDir, "user")
|
||||
const commands = await loadCommandsFromDir(userCommandsDir, "user")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export function loadProjectCommands(): Record<string, CommandDefinition> {
|
||||
export async function loadProjectCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const commands = loadCommandsFromDir(projectCommandsDir, "project")
|
||||
const commands = await loadCommandsFromDir(projectCommandsDir, "project")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export function loadOpencodeGlobalCommands(): Record<string, CommandDefinition> {
|
||||
const { homedir } = require("os")
|
||||
export async function loadOpencodeGlobalCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeCommandsDir = join(homedir(), ".config", "opencode", "command")
|
||||
const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode")
|
||||
const commands = await loadCommandsFromDir(opencodeCommandsDir, "opencode")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export function loadOpencodeProjectCommands(): Record<string, CommandDefinition> {
|
||||
export async function loadOpencodeProjectCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
const commands = await loadCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
async function loadCommandsFromDirAsync(
|
||||
commandsDir: string,
|
||||
scope: CommandScope,
|
||||
visited: Set<string> = new Set(),
|
||||
prefix: string = ""
|
||||
): Promise<LoadedCommand[]> {
|
||||
try {
|
||||
await fsPromises.access(commandsDir)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
let realPath: string
|
||||
try {
|
||||
realPath = await fsPromises.realpath(commandsDir)
|
||||
} catch (error) {
|
||||
log(`Failed to resolve command directory: ${commandsDir}`, error)
|
||||
return []
|
||||
}
|
||||
|
||||
if (visited.has(realPath)) {
|
||||
return []
|
||||
}
|
||||
visited.add(realPath)
|
||||
|
||||
let entries: Dirent[]
|
||||
try {
|
||||
entries = await fsPromises.readdir(commandsDir, { withFileTypes: true })
|
||||
} catch (error) {
|
||||
log(`Failed to read command directory: ${commandsDir}`, error)
|
||||
return []
|
||||
}
|
||||
|
||||
const commands: LoadedCommand[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name.startsWith(".")) continue
|
||||
const subDirPath = join(commandsDir, entry.name)
|
||||
const subPrefix = prefix ? `${prefix}:${entry.name}` : entry.name
|
||||
const subCommands = await loadCommandsFromDirAsync(subDirPath, scope, visited, subPrefix)
|
||||
commands.push(...subCommands)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isMarkdownFile(entry)) continue
|
||||
|
||||
const commandPath = join(commandsDir, entry.name)
|
||||
const baseCommandName = basename(entry.name, ".md")
|
||||
const commandName = prefix ? `${prefix}:${baseCommandName}` : baseCommandName
|
||||
|
||||
try {
|
||||
const content = await fsPromises.readFile(commandPath, "utf-8")
|
||||
const { data, body } = parseFrontmatter<CommandFrontmatter>(content)
|
||||
|
||||
const wrappedTemplate = `<command-instruction>
|
||||
${body.trim()}
|
||||
</command-instruction>
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>`
|
||||
|
||||
const formattedDescription = `(${scope}) ${data.description || ""}`
|
||||
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const definition: CommandDefinition = {
|
||||
name: commandName,
|
||||
description: formattedDescription,
|
||||
template: wrappedTemplate,
|
||||
agent: data.agent,
|
||||
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||
subtask: data.subtask,
|
||||
argumentHint: data["argument-hint"],
|
||||
handoffs: data.handoffs,
|
||||
}
|
||||
|
||||
commands.push({
|
||||
name: commandName,
|
||||
path: commandPath,
|
||||
definition,
|
||||
scope,
|
||||
})
|
||||
} catch (error) {
|
||||
log(`Failed to parse command: ${commandPath}`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
export async function loadUserCommandsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const commands = await loadCommandsFromDirAsync(userCommandsDir, "user")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export async function loadProjectCommandsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const commands = await loadCommandsFromDirAsync(projectCommandsDir, "project")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export async function loadOpencodeGlobalCommandsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const { homedir } = require("os")
|
||||
const opencodeCommandsDir = join(homedir(), ".config", "opencode", "command")
|
||||
const commands = await loadCommandsFromDirAsync(opencodeCommandsDir, "opencode")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export async function loadOpencodeProjectCommandsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
const commands = await loadCommandsFromDirAsync(opencodeProjectDir, "opencode-project")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export async function loadAllCommandsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
export async function loadAllCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const [user, project, global, projectOpencode] = await Promise.all([
|
||||
loadUserCommandsAsync(),
|
||||
loadProjectCommandsAsync(),
|
||||
loadOpencodeGlobalCommandsAsync(),
|
||||
loadOpencodeProjectCommandsAsync(),
|
||||
loadUserCommands(),
|
||||
loadProjectCommands(),
|
||||
loadOpencodeGlobalCommands(),
|
||||
loadOpencodeProjectCommands(),
|
||||
])
|
||||
return { ...projectOpencode, ...global, ...project, ...user }
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ This is the skill body.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "test-skill")
|
||||
|
||||
// #then
|
||||
@@ -89,7 +89,7 @@ This is a simple skill.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "simple-skill")
|
||||
|
||||
// #then
|
||||
@@ -122,7 +122,7 @@ Skill with env vars.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "env-skill")
|
||||
|
||||
// #then
|
||||
@@ -149,7 +149,7 @@ Skill body.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
// #then - when YAML fails, skill uses directory name as fallback
|
||||
const skill = skills.find(s => s.name === "bad-yaml-skill")
|
||||
|
||||
@@ -186,7 +186,7 @@ Skill body.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "ampcode-skill")
|
||||
|
||||
// #then
|
||||
@@ -227,7 +227,7 @@ Skill body.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "priority-skill")
|
||||
|
||||
// #then - mcp.json should take priority
|
||||
@@ -259,7 +259,7 @@ Skill body.
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||
const skill = skills.find(s => s.name === "direct-format")
|
||||
|
||||
// #then
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { promises as fs } from "fs"
|
||||
import { join, basename } from "path"
|
||||
import { homedir } from "os"
|
||||
import yaml from "js-yaml"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
|
||||
import { resolveSymlinkAsync, isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||
import type { SkillScope, SkillMetadata, LoadedSkill, LazyContentLoader } from "./types"
|
||||
@@ -26,20 +25,17 @@ function parseSkillMcpConfigFromFrontmatter(content: string): SkillMcpConfig | u
|
||||
return undefined
|
||||
}
|
||||
|
||||
function loadMcpJsonFromDir(skillDir: string): SkillMcpConfig | undefined {
|
||||
async function loadMcpJsonFromDir(skillDir: string): Promise<SkillMcpConfig | undefined> {
|
||||
const mcpJsonPath = join(skillDir, "mcp.json")
|
||||
if (!existsSync(mcpJsonPath)) return undefined
|
||||
|
||||
try {
|
||||
const content = readFileSync(mcpJsonPath, "utf-8")
|
||||
const content = await fs.readFile(mcpJsonPath, "utf-8")
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>
|
||||
|
||||
// AmpCode format: { "mcpServers": { "name": { ... } } }
|
||||
if (parsed && typeof parsed === "object" && "mcpServers" in parsed && parsed.mcpServers) {
|
||||
return parsed.mcpServers as SkillMcpConfig
|
||||
}
|
||||
|
||||
// Also support direct format: { "name": { command: ..., args: ... } }
|
||||
if (parsed && typeof parsed === "object" && !("mcpServers" in parsed)) {
|
||||
const hasCommandField = Object.values(parsed).some(
|
||||
(v) => v && typeof v === "object" && "command" in (v as Record<string, unknown>)
|
||||
@@ -59,77 +55,7 @@ function parseAllowedTools(allowedTools: string | undefined): string[] | undefin
|
||||
return allowedTools.split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
function loadSkillFromPath(
|
||||
skillPath: string,
|
||||
resolvedPath: string,
|
||||
defaultName: string,
|
||||
scope: SkillScope
|
||||
): LoadedSkill | null {
|
||||
try {
|
||||
const content = readFileSync(skillPath, "utf-8")
|
||||
const { data } = parseFrontmatter<SkillMetadata>(content)
|
||||
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
|
||||
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
|
||||
const mcpConfig = mcpJsonMcp || frontmatterMcp
|
||||
|
||||
const skillName = data.name || defaultName
|
||||
const originalDescription = data.description || ""
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||
|
||||
// Lazy content loader - only loads template on first use
|
||||
const lazyContent: LazyContentLoader = {
|
||||
loaded: false,
|
||||
content: undefined,
|
||||
load: async () => {
|
||||
if (!lazyContent.loaded) {
|
||||
const fileContent = await fs.readFile(skillPath, "utf-8")
|
||||
const { body } = parseFrontmatter<SkillMetadata>(fileContent)
|
||||
lazyContent.content = `<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>`
|
||||
lazyContent.loaded = true
|
||||
}
|
||||
return lazyContent.content!
|
||||
},
|
||||
}
|
||||
|
||||
const definition: CommandDefinition = {
|
||||
name: skillName,
|
||||
description: formattedDescription,
|
||||
template: "", // Empty at startup, loaded lazily
|
||||
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,
|
||||
license: data.license,
|
||||
compatibility: data.compatibility,
|
||||
metadata: data.metadata,
|
||||
allowedTools: parseAllowedTools(data["allowed-tools"]),
|
||||
mcpConfig,
|
||||
lazyContent,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillFromPathAsync(
|
||||
async function loadSkillFromPath(
|
||||
skillPath: string,
|
||||
resolvedPath: string,
|
||||
defaultName: string,
|
||||
@@ -139,7 +65,7 @@ async function loadSkillFromPathAsync(
|
||||
const content = await fs.readFile(skillPath, "utf-8")
|
||||
const { data } = parseFrontmatter<SkillMetadata>(content)
|
||||
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
|
||||
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
|
||||
const mcpJsonMcp = await loadMcpJsonFromDir(resolvedPath)
|
||||
const mcpConfig = mcpJsonMcp || frontmatterMcp
|
||||
|
||||
const skillName = data.name || defaultName
|
||||
@@ -198,60 +124,7 @@ $ARGUMENTS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of loadSkillsFromDir using Promise-based fs operations.
|
||||
*/
|
||||
async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
|
||||
async function loadSkillsFromDir(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
|
||||
const skills: LoadedSkill[] = []
|
||||
|
||||
@@ -261,13 +134,13 @@ async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Pro
|
||||
const entryPath = join(skillsDir, entry.name)
|
||||
|
||||
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
||||
const resolvedPath = resolveSymlink(entryPath)
|
||||
const resolvedPath = await resolveSymlinkAsync(entryPath)
|
||||
const dirName = entry.name
|
||||
|
||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||
try {
|
||||
await fs.access(skillMdPath)
|
||||
const skill = await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, scope)
|
||||
const skill = await loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope)
|
||||
if (skill) skills.push(skill)
|
||||
continue
|
||||
} catch {
|
||||
@@ -276,7 +149,7 @@ async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Pro
|
||||
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||
try {
|
||||
await fs.access(namedSkillMdPath)
|
||||
const skill = await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, scope)
|
||||
const skill = await loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope)
|
||||
if (skill) skills.push(skill)
|
||||
continue
|
||||
} catch {
|
||||
@@ -287,7 +160,7 @@ async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Pro
|
||||
|
||||
if (isMarkdownFile(entry)) {
|
||||
const skillName = basename(entry.name, ".md")
|
||||
const skill = await loadSkillFromPathAsync(entryPath, skillsDir, skillName, scope)
|
||||
const skill = await loadSkillFromPath(entryPath, skillsDir, skillName, scope)
|
||||
if (skill) skills.push(skill)
|
||||
}
|
||||
}
|
||||
@@ -304,190 +177,86 @@ function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from Claude Code user directory (~/.claude/skills/)
|
||||
*/
|
||||
export function loadUserSkills(): Record<string, CommandDefinition> {
|
||||
export async function loadUserSkills(): Promise<Record<string, CommandDefinition>> {
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
const skills = loadSkillsFromDir(userSkillsDir, "user")
|
||||
const skills = await loadSkillsFromDir(userSkillsDir, "user")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from Claude Code project directory (.claude/skills/)
|
||||
*/
|
||||
export function loadProjectSkills(): Record<string, CommandDefinition> {
|
||||
export async function loadProjectSkills(): Promise<Record<string, CommandDefinition>> {
|
||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||
const skills = loadSkillsFromDir(projectSkillsDir, "project")
|
||||
const skills = await loadSkillsFromDir(projectSkillsDir, "project")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from OpenCode global directory (~/.config/opencode/skill/)
|
||||
*/
|
||||
export function loadOpencodeGlobalSkills(): Record<string, CommandDefinition> {
|
||||
export async function loadOpencodeGlobalSkills(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||
const skills = loadSkillsFromDir(opencodeSkillsDir, "opencode")
|
||||
const skills = await loadSkillsFromDir(opencodeSkillsDir, "opencode")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from OpenCode project directory (.opencode/skill/)
|
||||
*/
|
||||
export function loadOpencodeProjectSkills(): Record<string, CommandDefinition> {
|
||||
export async function loadOpencodeProjectSkills(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
const skills = loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||
const skills = await loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of loadUserSkills
|
||||
*/
|
||||
export async function loadUserSkillsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
const skills = await loadSkillsFromDirAsync(userSkillsDir, "user")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of loadProjectSkills
|
||||
*/
|
||||
export async function loadProjectSkillsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||
const skills = await loadSkillsFromDirAsync(projectSkillsDir, "project")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of loadOpencodeGlobalSkills
|
||||
*/
|
||||
export async function loadOpencodeGlobalSkillsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||
const skills = await loadSkillsFromDirAsync(opencodeSkillsDir, "opencode")
|
||||
return skillsToRecord(skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of loadOpencodeProjectSkills
|
||||
*/
|
||||
export async function loadOpencodeProjectSkillsAsync(): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
const skills = await loadSkillsFromDirAsync(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]
|
||||
}
|
||||
|
||||
export interface DiscoverSkillsOptions {
|
||||
includeClaudeCodePaths?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover skills with optional filtering.
|
||||
* When includeClaudeCodePaths is false, only loads from OpenCode paths.
|
||||
*/
|
||||
export function discoverSkills(options: DiscoverSkillsOptions = {}): LoadedSkill[] {
|
||||
const { includeClaudeCodePaths = true } = options
|
||||
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
const opencodeGlobalDir = join(homedir(), ".config", "opencode", "skill")
|
||||
|
||||
const opencodeProjectSkills = loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||
const opencodeGlobalSkills = loadSkillsFromDir(opencodeGlobalDir, "opencode")
|
||||
|
||||
if (!includeClaudeCodePaths) {
|
||||
return [...opencodeProjectSkills, ...opencodeGlobalSkills]
|
||||
}
|
||||
|
||||
const projectDir = join(process.cwd(), ".claude", "skills")
|
||||
const userDir = join(getClaudeConfigDir(), "skills")
|
||||
|
||||
const projectSkills = loadSkillsFromDir(projectDir, "project")
|
||||
const userSkills = loadSkillsFromDir(userDir, "user")
|
||||
export async function discoverAllSkills(): Promise<LoadedSkill[]> {
|
||||
const [opencodeProjectSkills, projectSkills, opencodeGlobalSkills, userSkills] = await Promise.all([
|
||||
discoverOpencodeProjectSkills(),
|
||||
discoverProjectClaudeSkills(),
|
||||
discoverOpencodeGlobalSkills(),
|
||||
discoverUserClaudeSkills(),
|
||||
])
|
||||
|
||||
return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a skill by name from all available sources.
|
||||
*/
|
||||
export function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): LoadedSkill | undefined {
|
||||
const skills = discoverSkills(options)
|
||||
return skills.find(s => s.name === name)
|
||||
}
|
||||
|
||||
export function discoverUserClaudeSkills(): LoadedSkill[] {
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
return loadSkillsFromDir(userSkillsDir, "user")
|
||||
}
|
||||
|
||||
export function discoverProjectClaudeSkills(): LoadedSkill[] {
|
||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||
return loadSkillsFromDir(projectSkillsDir, "project")
|
||||
}
|
||||
|
||||
export function discoverOpencodeGlobalSkills(): LoadedSkill[] {
|
||||
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||
return loadSkillsFromDir(opencodeSkillsDir, "opencode")
|
||||
}
|
||||
|
||||
export function discoverOpencodeProjectSkills(): LoadedSkill[] {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
return loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||
}
|
||||
|
||||
export async function discoverUserClaudeSkillsAsync(): Promise<LoadedSkill[]> {
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
return loadSkillsFromDirAsync(userSkillsDir, "user")
|
||||
}
|
||||
|
||||
export async function discoverProjectClaudeSkillsAsync(): Promise<LoadedSkill[]> {
|
||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||
return loadSkillsFromDirAsync(projectSkillsDir, "project")
|
||||
}
|
||||
|
||||
export async function discoverOpencodeGlobalSkillsAsync(): Promise<LoadedSkill[]> {
|
||||
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||
return loadSkillsFromDirAsync(opencodeSkillsDir, "opencode")
|
||||
}
|
||||
|
||||
export async function discoverOpencodeProjectSkillsAsync(): Promise<LoadedSkill[]> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
return loadSkillsFromDirAsync(opencodeProjectDir, "opencode-project")
|
||||
}
|
||||
|
||||
export async function discoverAllSkillsAsync(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
||||
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
||||
const { includeClaudeCodePaths = true } = options
|
||||
|
||||
const opencodeProjectSkills = await discoverOpencodeProjectSkillsAsync()
|
||||
const opencodeGlobalSkills = await discoverOpencodeGlobalSkillsAsync()
|
||||
const [opencodeProjectSkills, opencodeGlobalSkills] = await Promise.all([
|
||||
discoverOpencodeProjectSkills(),
|
||||
discoverOpencodeGlobalSkills(),
|
||||
])
|
||||
|
||||
if (!includeClaudeCodePaths) {
|
||||
return [...opencodeProjectSkills, ...opencodeGlobalSkills]
|
||||
}
|
||||
|
||||
const [projectSkills, userSkills] = await Promise.all([
|
||||
discoverProjectClaudeSkillsAsync(),
|
||||
discoverUserClaudeSkillsAsync(),
|
||||
discoverProjectClaudeSkills(),
|
||||
discoverUserClaudeSkills(),
|
||||
])
|
||||
|
||||
return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]
|
||||
}
|
||||
|
||||
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> {
|
||||
const skills = await discoverSkills(options)
|
||||
return skills.find(s => s.name === name)
|
||||
}
|
||||
|
||||
export async function discoverUserClaudeSkills(): Promise<LoadedSkill[]> {
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
return loadSkillsFromDir(userSkillsDir, "user")
|
||||
}
|
||||
|
||||
export async function discoverProjectClaudeSkills(): Promise<LoadedSkill[]> {
|
||||
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||
return loadSkillsFromDir(projectSkillsDir, "project")
|
||||
}
|
||||
|
||||
export async function discoverOpencodeGlobalSkills(): Promise<LoadedSkill[]> {
|
||||
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||
return loadSkillsFromDir(opencodeSkillsDir, "opencode")
|
||||
}
|
||||
|
||||
export async function discoverOpencodeProjectSkills(): Promise<LoadedSkill[]> {
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||
return loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||
}
|
||||
}
|
||||
|
||||
function discoverAllCommands(): CommandInfo[] {
|
||||
async function discoverAllCommands(): Promise<CommandInfo[]> {
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(homedir(), ".config", "opencode", "command")
|
||||
@@ -105,7 +105,7 @@ function discoverAllCommands(): CommandInfo[] {
|
||||
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project")
|
||||
const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
|
||||
const skills = discoverAllSkills()
|
||||
const skills = await discoverAllSkills()
|
||||
const skillCommands = skills.map(skillToCommandInfo)
|
||||
|
||||
return [
|
||||
@@ -117,8 +117,8 @@ function discoverAllCommands(): CommandInfo[] {
|
||||
]
|
||||
}
|
||||
|
||||
function findCommand(commandName: string): CommandInfo | null {
|
||||
const allCommands = discoverAllCommands()
|
||||
async function findCommand(commandName: string): Promise<CommandInfo | null> {
|
||||
const allCommands = await discoverAllCommands()
|
||||
return allCommands.find(
|
||||
(cmd) => cmd.name.toLowerCase() === commandName.toLowerCase()
|
||||
) ?? null
|
||||
@@ -170,7 +170,7 @@ export interface ExecuteResult {
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(parsed: ParsedSlashCommand): Promise<ExecuteResult> {
|
||||
const command = findCommand(parsed.command)
|
||||
const command = await findCommand(parsed.command)
|
||||
|
||||
if (!command) {
|
||||
return {
|
||||
|
||||
16
src/index.ts
16
src/index.ts
@@ -34,10 +34,10 @@ import {
|
||||
} from "./features/context-injector";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
discoverUserClaudeSkillsAsync,
|
||||
discoverProjectClaudeSkillsAsync,
|
||||
discoverOpencodeGlobalSkillsAsync,
|
||||
discoverOpencodeProjectSkillsAsync,
|
||||
discoverUserClaudeSkills,
|
||||
discoverProjectClaudeSkills,
|
||||
discoverOpencodeGlobalSkills,
|
||||
discoverOpencodeProjectSkills,
|
||||
mergeSkills,
|
||||
} from "./features/opencode-skill-loader";
|
||||
import { createBuiltinSkills } from "./features/builtin-skills";
|
||||
@@ -205,10 +205,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
});
|
||||
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false;
|
||||
const [userSkills, globalSkills, projectSkills, opencodeProjectSkills] = await Promise.all([
|
||||
includeClaudeSkills ? discoverUserClaudeSkillsAsync() : Promise.resolve([]),
|
||||
discoverOpencodeGlobalSkillsAsync(),
|
||||
includeClaudeSkills ? discoverProjectClaudeSkillsAsync() : Promise.resolve([]),
|
||||
discoverOpencodeProjectSkillsAsync(),
|
||||
includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]),
|
||||
discoverOpencodeGlobalSkills(),
|
||||
includeClaudeSkills ? discoverProjectClaudeSkills() : Promise.resolve([]),
|
||||
discoverOpencodeProjectSkills(),
|
||||
]);
|
||||
const mergedSkills = mergeSkills(
|
||||
builtinSkills,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { createBuiltinAgents } from "../agents";
|
||||
import {
|
||||
loadUserCommandsAsync,
|
||||
loadProjectCommandsAsync,
|
||||
loadOpencodeGlobalCommandsAsync,
|
||||
loadOpencodeProjectCommandsAsync,
|
||||
loadUserCommands,
|
||||
loadProjectCommands,
|
||||
loadOpencodeGlobalCommands,
|
||||
loadOpencodeProjectCommands,
|
||||
} from "../features/claude-code-command-loader";
|
||||
import { loadBuiltinCommands } from "../features/builtin-commands";
|
||||
import {
|
||||
loadUserSkillsAsync,
|
||||
loadProjectSkillsAsync,
|
||||
loadOpencodeGlobalSkillsAsync,
|
||||
loadOpencodeProjectSkillsAsync,
|
||||
loadUserSkills,
|
||||
loadProjectSkills,
|
||||
loadOpencodeGlobalSkills,
|
||||
loadOpencodeProjectSkills,
|
||||
} from "../features/opencode-skill-loader";
|
||||
import {
|
||||
loadUserAgents,
|
||||
@@ -298,14 +298,14 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
opencodeGlobalSkills,
|
||||
opencodeProjectSkills,
|
||||
] = await Promise.all([
|
||||
includeClaudeCommands ? loadUserCommandsAsync() : Promise.resolve({}),
|
||||
includeClaudeCommands ? loadProjectCommandsAsync() : Promise.resolve({}),
|
||||
loadOpencodeGlobalCommandsAsync(),
|
||||
loadOpencodeProjectCommandsAsync(),
|
||||
includeClaudeSkills ? loadUserSkillsAsync() : Promise.resolve({}),
|
||||
includeClaudeSkills ? loadProjectSkillsAsync() : Promise.resolve({}),
|
||||
loadOpencodeGlobalSkillsAsync(),
|
||||
loadOpencodeProjectSkillsAsync(),
|
||||
includeClaudeCommands ? loadUserCommands() : Promise.resolve({}),
|
||||
includeClaudeCommands ? loadProjectCommands() : Promise.resolve({}),
|
||||
loadOpencodeGlobalCommands(),
|
||||
loadOpencodeProjectCommands(),
|
||||
includeClaudeSkills ? loadUserSkills() : Promise.resolve({}),
|
||||
includeClaudeSkills ? loadProjectSkills() : Promise.resolve({}),
|
||||
loadOpencodeGlobalSkills(),
|
||||
loadOpencodeProjectSkills(),
|
||||
]);
|
||||
|
||||
config.command = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { lstatSync, readlinkSync } from "fs"
|
||||
import { promises as fs } from "fs"
|
||||
import { resolve } from "path"
|
||||
|
||||
export function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
|
||||
@@ -24,3 +25,16 @@ export function resolveSymlink(filePath: string): string {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveSymlinkAsync(filePath: string): Promise<string> {
|
||||
try {
|
||||
const stats = await fs.lstat(filePath)
|
||||
if (stats.isSymbolicLink()) {
|
||||
const linkTarget = await fs.readlink(filePath)
|
||||
return resolve(filePath, "..", linkTarget)
|
||||
}
|
||||
return filePath
|
||||
} catch {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,22 +129,38 @@ async function formatMcpCapabilities(
|
||||
}
|
||||
|
||||
export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition {
|
||||
const skills = options.skills ?? discoverSkills({ includeClaudeCodePaths: !options.opencodeOnly })
|
||||
const skillInfos = skills.map(loadedSkillToInfo)
|
||||
let cachedSkills: LoadedSkill[] | null = null
|
||||
let cachedDescription: string | null = null
|
||||
|
||||
const description = skillInfos.length === 0
|
||||
const getSkills = async (): Promise<LoadedSkill[]> => {
|
||||
if (options.skills) return options.skills
|
||||
if (cachedSkills) return cachedSkills
|
||||
cachedSkills = await discoverSkills({ includeClaudeCodePaths: !options.opencodeOnly })
|
||||
return cachedSkills
|
||||
}
|
||||
|
||||
const getDescription = async (): Promise<string> => {
|
||||
if (cachedDescription) return cachedDescription
|
||||
const skills = await getSkills()
|
||||
const skillInfos = skills.map(loadedSkillToInfo)
|
||||
cachedDescription = skillInfos.length === 0
|
||||
? TOOL_DESCRIPTION_NO_SKILLS
|
||||
: TOOL_DESCRIPTION_PREFIX + formatSkillsXml(skillInfos)
|
||||
return cachedDescription
|
||||
}
|
||||
|
||||
getDescription()
|
||||
|
||||
return tool({
|
||||
description,
|
||||
get description() {
|
||||
return cachedDescription ?? TOOL_DESCRIPTION_PREFIX
|
||||
},
|
||||
args: {
|
||||
name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'code-review')"),
|
||||
},
|
||||
async execute(args: SkillArgs) {
|
||||
const skill = options.skills
|
||||
? skills.find(s => s.name === args.name)
|
||||
: skills.find(s => s.name === args.name)
|
||||
const skills = await getSkills()
|
||||
const skill = skills.find(s => s.name === args.name)
|
||||
|
||||
if (!skill) {
|
||||
const available = skills.map(s => s.name).join(", ")
|
||||
|
||||
@@ -83,19 +83,6 @@ function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||
}
|
||||
}
|
||||
|
||||
const availableCommands = discoverCommandsSync()
|
||||
const availableSkills = discoverAllSkills()
|
||||
const availableItems = [
|
||||
...availableCommands,
|
||||
...availableSkills.map(skillToCommandInfo),
|
||||
]
|
||||
const commandListForDescription = availableItems
|
||||
.map((cmd) => {
|
||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||
return `- /${cmd.name}${hint}: ${cmd.metadata.description} (${cmd.scope})`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
async function formatLoadedCommand(cmd: CommandInfo): Promise<string> {
|
||||
const sections: string[] = []
|
||||
|
||||
@@ -151,15 +138,40 @@ function formatCommandList(items: CommandInfo[]): string {
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export const slashcommand: ToolDefinition = tool({
|
||||
description: `Load a skill to get detailed instructions for a specific task.
|
||||
async function buildDescription(): Promise<string> {
|
||||
const availableCommands = discoverCommandsSync()
|
||||
const availableSkills = await discoverAllSkills()
|
||||
const availableItems = [
|
||||
...availableCommands,
|
||||
...availableSkills.map(skillToCommandInfo),
|
||||
]
|
||||
const commandListForDescription = availableItems
|
||||
.map((cmd) => {
|
||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||
return `- /${cmd.name}${hint}: ${cmd.metadata.description} (${cmd.scope})`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
return `Load a skill to get detailed instructions for a specific task.
|
||||
|
||||
Skills provide specialized knowledge and step-by-step guidance.
|
||||
Use this when a task matches an available skill's description.
|
||||
|
||||
<available_skills>
|
||||
${commandListForDescription}
|
||||
</available_skills>`,
|
||||
</available_skills>`
|
||||
}
|
||||
|
||||
let cachedDescription: string | null = null
|
||||
|
||||
export const slashcommand: ToolDefinition = tool({
|
||||
get description() {
|
||||
if (!cachedDescription) {
|
||||
cachedDescription = "Loading available commands and skills..."
|
||||
buildDescription().then(desc => { cachedDescription = desc })
|
||||
}
|
||||
return cachedDescription
|
||||
},
|
||||
|
||||
args: {
|
||||
command: tool.schema
|
||||
@@ -171,7 +183,7 @@ ${commandListForDescription}
|
||||
|
||||
async execute(args) {
|
||||
const commands = discoverCommandsSync()
|
||||
const skills = discoverAllSkills()
|
||||
const skills = await discoverAllSkills()
|
||||
const allItems = [
|
||||
...commands,
|
||||
...skills.map(skillToCommandInfo),
|
||||
|
||||
Reference in New Issue
Block a user