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 = '' export const LIST_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":"","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 }