Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/omo-opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createPluginModule } from "./testing/create-plugin-module"

const pluginModule: PluginModule = createPluginModule()

export default pluginModule
export const omoPlugin = pluginModule.server

export type {
AgentName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,16 @@ export async function loadSkillFromPath(options: {
lazyContent: eagerLoader,
}
} catch (error) {
console.error(`[loadSkillFromPath] FAILED: ${options.skillPath}`)
if (error instanceof Error) {
return null
console.error(`[loadSkillFromPath] Error type: ${error.constructor.name}`)
console.error(`[loadSkillFromPath] Message: ${error.message}`)
if (error.stack) {
console.error(`[loadSkillFromPath] Stack:\n${error.stack}`)
}
} else {
console.error(`[loadSkillFromPath] Non-Error thrown: ${typeof error}`)
console.error(`[loadSkillFromPath] Value:`, error)
}
return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export async function loadSkillsFromDir(options: {
const depth = options.depth ?? 0
const maxDepth = options.maxDepth ?? 2

console.error(`[loadSkillsFromDir] Scanning: ${options.skillsDir} (depth=${depth}, scope=${options.scope})`)

const entries = await readDirectoryEntries(options.skillsDir)
console.error(`[loadSkillsFromDir] Found ${entries.length} entries in ${options.skillsDir}`)

const skillMap = new Map<string, LoadedSkill>()

const directories = entries.filter(
Expand All @@ -53,13 +57,22 @@ export async function loadSkillsFromDir(options: {
isMarkdownFile(entry)
)

console.error(`[loadSkillsFromDir] ${directories.length} directories, ${files.length} markdown files`)

for (const entry of directories) {
const entryPath = join(options.skillsDir, entry.name)
const resolvedPath = await resolveSymlinkAsync(entryPath)
const dirName = entry.name

const skillMdPath = join(resolvedPath, "SKILL.md")
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)

console.error(`[loadSkillsFromDir] Checking directory: ${dirName}`)
console.error(`[loadSkillsFromDir] SKILL.md path: ${skillMdPath}`)
console.error(`[loadSkillsFromDir] ${dirName}.md path: ${namedSkillMdPath}`)

if (await canAccessFile(skillMdPath)) {
console.error(`[loadSkillsFromDir] Found SKILL.md, loading...`)
const skill = await loadSkillFromPath({
skillPath: skillMdPath,
resolvedPath,
Expand All @@ -68,13 +81,18 @@ export async function loadSkillsFromDir(options: {
namePrefix,
})
if (skill && !skillMap.has(skill.name)) {
console.error(`[loadSkillsFromDir] ✓ Loaded skill: ${skill.name}`)
skillMap.set(skill.name, skill)
} else if (!skill) {
console.error(`[loadSkillsFromDir] ✗ loadSkillFromPath returned null for ${dirName}`)
} else {
console.error(`[loadSkillsFromDir] ⊘ Skill ${skill.name} already exists in map, skipping`)
}
continue
}

const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
if (await canAccessFile(namedSkillMdPath)) {
console.error(`[loadSkillsFromDir] Found ${dirName}.md, loading...`)
const skill = await loadSkillFromPath({
skillPath: namedSkillMdPath,
resolvedPath,
Expand All @@ -83,11 +101,18 @@ export async function loadSkillsFromDir(options: {
namePrefix,
})
if (skill && !skillMap.has(skill.name)) {
console.error(`[loadSkillsFromDir] ✓ Loaded skill: ${skill.name}`)
skillMap.set(skill.name, skill)
} else if (!skill) {
console.error(`[loadSkillsFromDir] ✗ loadSkillFromPath returned null for ${dirName}`)
} else {
console.error(`[loadSkillsFromDir] ⊘ Skill ${skill.name} already exists in map, skipping`)
}
continue
}

console.error(`[loadSkillsFromDir] No SKILL.md or ${dirName}.md found, checking nested...`)

if (depth < maxDepth) {
const newPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName
const nestedSkills = await loadSkillsFromDir({
Expand All @@ -97,6 +122,7 @@ export async function loadSkillsFromDir(options: {
depth: depth + 1,
maxDepth,
})
console.error(`[loadSkillsFromDir] Found ${nestedSkills.length} nested skills`)
for (const nestedSkill of nestedSkills) {
if (!skillMap.has(nestedSkill.name)) {
skillMap.set(nestedSkill.name, nestedSkill)
Expand All @@ -108,6 +134,7 @@ export async function loadSkillsFromDir(options: {
for (const entry of files) {
const entryPath = join(options.skillsDir, entry.name)
const baseName = inferSkillNameFromFileName(entryPath)
console.error(`[loadSkillsFromDir] Loading markdown file: ${entry.name}`)
const skill = await loadSkillFromPath({
skillPath: entryPath,
resolvedPath: options.skillsDir,
Expand All @@ -116,9 +143,13 @@ export async function loadSkillsFromDir(options: {
namePrefix,
})
if (skill && !skillMap.has(skill.name)) {
console.error(`[loadSkillsFromDir] ✓ Loaded skill: ${skill.name}`)
skillMap.set(skill.name, skill)
} else if (!skill) {
console.error(`[loadSkillsFromDir] ✗ loadSkillFromPath returned null for ${entry.name}`)
}
}

console.error(`[loadSkillsFromDir] Completed: ${skillMap.size} skills loaded from ${options.skillsDir}`)
return Array.from(skillMap.values())
}
73 changes: 73 additions & 0 deletions packages/skills-loader-core/test-downstream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as fs from "node:fs/promises"
import * as path from "path"
import { parseFrontmatter } from "@oh-my-opencode/utils"
import { parseSkillMcpConfigFromFrontmatter, loadMcpJsonFromDir } from "./src/features/opencode-skill-loader/skill-mcp-config"
import { resolveSkillPathReferences } from "./src/shared/skill-path-resolver"
import { sanitizeModelField } from "@oh-my-opencode/model-core"
import { parseAllowedTools } from "./src/features/opencode-skill-loader/allowed-tools-parser"

const SKILLS_DIR = "/root/.config/opencode/skills"

async function testDownstream(name: string) {
const skillPath = path.join(SKILLS_DIR, name, "SKILL.md")
const resolvedPath = path.join(SKILLS_DIR, name)

console.log(`\n=== ${name} ===`)

const content = await fs.readFile(skillPath, "utf-8")

// 1. Test parseFrontmatter
try {
const { data, body } = parseFrontmatter(content)
console.log(` [1] parseFrontmatter: OK (name=${data.name})`)

// 2. Test parseSkillMcpConfigFromFrontmatter
try {
const mcpConfig = parseSkillMcpConfigFromFrontmatter(content)
console.log(` [2] parseSkillMcpConfigFromFrontmatter: OK`)
} catch (err: any) {
console.log(` [2] parseSkillMcpConfigFromFrontmatter: FAILED - ${err.message}`)
}

// 3. Test loadMcpJsonFromDir
try {
const mcpJson = await loadMcpJsonFromDir(resolvedPath)
console.log(` [3] loadMcpJsonFromDir: OK`)
} catch (err: any) {
console.log(` [3] loadMcpJsonFromDir: FAILED - ${err.message}`)
}

// 4. Test resolveSkillPathReferences
try {
const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
console.log(` [4] resolveSkillPathReferences: OK (len=${resolvedBody.length})`)
} catch (err: any) {
console.log(` [4] resolveSkillPathReferences: FAILED - ${err.message}`)
}

// 5. Test sanitizeModelField
try {
const model = sanitizeModelField(data.model, "opencode")
console.log(` [5] sanitizeModelField: OK (model=${model})`)
} catch (err: any) {
console.log(` [5] sanitizeModelField: FAILED - ${err.message}`)
}

// 6. Test parseAllowedTools
try {
const allowedTools = parseAllowedTools(data["allowed-tools"])
console.log(` [6] parseAllowedTools: OK (tools=${JSON.stringify(allowedTools)})`)
} catch (err: any) {
console.log(` [6] parseAllowedTools: FAILED - ${err.message}`)
}

} catch (err: any) {
console.log(` [1] parseFrontmatter: FAILED - ${err.message}`)
}
}

const SKILLS = ["grilling", "grill-with-docs", "teach", "scaffold-exercises", "obsidian", "codex", "claude", "review"]

for (const skill of SKILLS) {
await testDownstream(skill)
}
70 changes: 70 additions & 0 deletions packages/skills-loader-core/test-matt-pocock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { loadSkillFromPath } from "./src/features/opencode-skill-loader/loaded-skill-from-path"
import { parseFrontmatter } from "@oh-my-opencode/utils"
import * as fs from "node:fs/promises"
import * as path from "path"

const SKILLS = [
"grilling",
"grill-with-docs",
"teach",
"scaffold-exercises",
"git-guardrails-claude-code",
"migrate-to-shoehorn",
"edit-article",
"obsidian-vault",
"decision-mapping",
"obsidian",
"codex",
"claude",
"review",
]

const SKILLS_DIR = path.resolve("/root/.config/opencode/skills")

async function testSkill(name: string) {
const skillPath = path.join(SKILLS_DIR, name, "SKILL.md")
const resolvedPath = path.join(SKILLS_DIR, name)

console.log(`\n=== Testing: ${name} ===`)
console.log(`Path: ${skillPath}`)

try {
const stat = await fs.stat(skillPath)
console.log(`File exists: ${stat.size} bytes`)

const content = await fs.readFile(skillPath, "utf-8")
console.log(`Content length: ${content.length}`)

const { data, body } = parseFrontmatter(content)
console.log(`Frontmatter parsed successfully:`)
console.log(` name: ${data.name}`)
console.log(` description: ${data.description?.substring(0, 80)}...`)
console.log(` model: ${data.model}`)
console.log(` license: ${data.license}`)
console.log(` compatibility: ${data.compatibility}`)

const result = await loadSkillFromPath({
skillPath,
resolvedPath,
defaultName: name,
scope: "opencode",
})

if (result) {
console.log(`loadSkillFromPath: SUCCESS`)
console.log(` result.name: ${result.name}`)
} else {
console.log(`loadSkillFromPath: returned null`)
}
} catch (error) {
console.error(`Test failed:`, error)
}
}

async function main() {
for (const skill of SKILLS) {
await testSkill(skill)
}
}

main()