Files
jsc-anima/hooks/skillgen.ts
T

113 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const PLUGIN = 'jsc-anima'
export const SKILL_PREFIX = 'anima-'
export const SKILL_DIR = 'skills'
export const GUIDE_SKILL = 'jsc-anima'
export const LIST_START = '<!-- skills:start -->'
export const LIST_END = '<!-- skills:end -->'
export type Parsed = { name: string; description: string; body: string }
export const slugify = (s: string): string =>
s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
export const commandOf = (skill: string): string => `/${PLUGIN}:${SKILL_PREFIX}${skill}`
// 指令第一個字是技能名稱,其餘是描述。
export const parseArgs = (args: string): { skill: string; description: string } | undefined => {
const m = args.trim().match(/^(\S+)\s+([\s\S]+)$/)
if (!m) return undefined
const skill = slugify(m[1]!)
return skill ? { skill, description: m[2]!.trim() } : undefined
}
export const parseSkillMd = (md: string): Parsed | undefined => {
const m = md.trim().match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
if (!m) return undefined
const field = (k: string) => m[1]!.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'))?.[1]?.trim().replace(/^["']|["']$/g, '')
return { name: field('name') ?? '', description: field('description') ?? '', body: m[2]!.trim() }
}
// 回傳錯誤訊息清單,空陣列代表通過。
export const validateSkill = (md: string, skill: string): string[] => {
const p = parseSkillMd(md)
if (!p) return ['缺少 frontmatter(--- 包住的 name 與 description)']
const errs: string[] = []
if (p.name !== `${SKILL_PREFIX}${skill}`) errs.push(`name 必須是 ${SKILL_PREFIX}${skill},實際為「${p.name}」`)
if (!p.description) errs.push('description 不可為空')
if (p.body.length < 20) errs.push('內文過短')
return errs
}
export const bumpVersion = (v: string, kind: 'minor' | 'patch'): string => {
const [a = 0, b = 0, c = 0] = v.split('.').map(n => Number(n) || 0)
return kind === 'minor' ? `${a}.${b + 1}.0` : `${a}.${b}.${c + 1}`
}
export type SkillEntry = { skill: string; description: string }
export const parseEntries = (readme: string): SkillEntry[] => {
const block = readme.split(LIST_START)[1]?.split(LIST_END)[0] ?? ''
const out: SkillEntry[] = []
for (const line of block.split('\n')) {
const m = line.match(/^- `\/jsc-anima:anima-([^`\s]+)[^`]*` — (.*)$/)
if (m) out.push({ skill: m[1]!, description: m[2]! })
}
return out
}
export const upsertEntry = (list: SkillEntry[], e: SkillEntry): SkillEntry[] =>
[...list.filter(x => x.skill !== e.skill), e].sort((a, b) => a.skill.localeCompare(b.skill))
// 技能 README:名稱 /jsc-anima:jsc-anima、技能列表(完整可複製指令)與範例。
export const renderGuide = (list: SkillEntry[], example?: { skill: string; input: string }): string => {
const ex = example ?? { skill: list[0]?.skill ?? 'example', input: '' }
return [
`# /${PLUGIN}:${GUIDE_SKILL}`,
'',
'動漫助理的使用說明。先用 `/jsc-anima:{作品代號}-{角色代號}` 啟動角色,再使用下列技能。',
'',
'## 技能列表',
'',
LIST_START,
...list.map(e => `- \`${commandOf(e.skill)}\` — ${e.description}`),
LIST_END,
'',
'## 範例',
'',
'```',
`${commandOf(ex.skill)}${ex.input ? ' ' + ex.input : ''}`,
'```',
'',
'產生新技能:`/jsc-anima:anima-skill <技能名稱> <描述>`,通過驗證與實跑後自動對預設分支建立 PR。',
'',
].join('\n')
}
export const guideSkillMd = (): string =>
`---\nname: ${GUIDE_SKILL}\ndescription: jsc-anima 動漫助理的使用說明與技能列表\n---\n\n請讀取同目錄的 README.md 並依內容回答使用者。\n`
export const setReadmeVersion = (readme: string, version: string): string => {
const line = `目前版本:${version}`
return /^目前版本:.*$/m.test(readme)
? readme.replace(/^目前版本:.*$/m, line)
: readme.replace(/^(# jsc-anima\n)/m, `$1\n${line}\n`)
}
export const setPluginVersion = (json: string, version: string): string =>
json.replace(/("version":\s*")[^"]*(")/, `$1${version}$2`)
export const generatePrompt = (skill: string, description: string): string =>
[
`請撰寫一個 Claude 技能,技能名稱 ${SKILL_PREFIX}${skill},用途:${description}`,
'只輸出 JSON(不要程式碼圍欄),格式:{"skill":"<SKILL.md 全文,含 frontmatter name 與 description,內文用繁體中文>","example":"<一筆範例輸入>"}',
`frontmatter 的 name 必須是 ${SKILL_PREFIX}${skill}。`,
].join('\n')
export const parseGenerated = (text: string): { skill: string; example: string } | undefined => {
try {
const j = JSON.parse(text.trim().replace(/^```(?:json)?\n?|\n?```$/g, ''))
if (typeof j.skill === 'string' && typeof j.example === 'string') return { skill: j.skill, example: j.example }
} catch {}
return undefined
}