recur-skills 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code skills for integrating Recur - Taiwan's subscription payment platform",
9
- "version": "0.0.7"
9
+ "version": "0.0.8"
10
10
  },
11
11
  "plugins": [
12
12
  {
package/dist/cli.mjs CHANGED
@@ -76,8 +76,10 @@ function getSkillsDir() {
76
76
 
77
77
  //#endregion
78
78
  //#region src/cli.ts
79
- const SKILLS_SOURCE = join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
80
- program.name("recur-skills").description("Claude Code skills for Recur payment integration").version("0.0.1");
79
+ const __dirname = dirname(fileURLToPath(import.meta.url));
80
+ const SKILLS_SOURCE = join(__dirname, "..", "skills");
81
+ const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
82
+ program.name("recur-skills").description("Claude Code skills for Recur payment integration").version(version);
81
83
  program.command("list").description("List all available skills").action(() => {
82
84
  const skills = getAllSkills();
83
85
  if (skills.length === 0) {
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["data: Record<string, string>","skills: Skill[]","targetDir: string","toInstall: string[]"],"sources":["../src/index.ts","../src/cli.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst SKILLS_DIR = join(__dirname, '..', 'skills')\n\nexport interface SkillMetadata {\n name: string\n description: string\n}\n\nexport interface Skill extends SkillMetadata {\n slug: string\n content: string\n path: string\n}\n\n/**\n * Parse SKILL.md frontmatter\n */\nfunction parseFrontmatter(content: string): { data: Record<string, string>; content: string } {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/)\n if (!match) {\n return { data: {}, content }\n }\n\n const data: Record<string, string> = {}\n const lines = match[1].split('\\n')\n for (const line of lines) {\n const colonIndex = line.indexOf(':')\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim()\n const value = line.slice(colonIndex + 1).trim()\n data[key] = value\n }\n }\n\n return { data, content: match[2] }\n}\n\n/**\n * Get all available skills\n */\nexport function getAllSkills(): Skill[] {\n if (!existsSync(SKILLS_DIR)) {\n return []\n }\n\n const skillDirs = readdirSync(SKILLS_DIR, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory())\n .map(dirent => dirent.name)\n\n const skills: Skill[] = []\n\n for (const dir of skillDirs) {\n const skillPath = join(SKILLS_DIR, dir, 'SKILL.md')\n if (!existsSync(skillPath)) continue\n\n const rawContent = readFileSync(skillPath, 'utf-8')\n const { data, content } = parseFrontmatter(rawContent)\n\n skills.push({\n slug: dir,\n name: data.name || dir,\n description: data.description || '',\n content,\n path: skillPath,\n })\n }\n\n return skills\n}\n\n/**\n * Get a single skill by slug\n */\nexport function getSkill(slug: string): Skill | null {\n const skillPath = join(SKILLS_DIR, slug, 'SKILL.md')\n if (!existsSync(skillPath)) return null\n\n const rawContent = readFileSync(skillPath, 'utf-8')\n const { data, content } = parseFrontmatter(rawContent)\n\n return {\n slug,\n name: data.name || slug,\n description: data.description || '',\n content,\n path: skillPath,\n }\n}\n\n/**\n * Get skill names only (for listing)\n */\nexport function getSkillNames(): string[] {\n if (!existsSync(SKILLS_DIR)) {\n return []\n }\n\n return readdirSync(SKILLS_DIR, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory())\n .filter(dirent => existsSync(join(SKILLS_DIR, dirent.name, 'SKILL.md')))\n .map(dirent => dirent.name)\n}\n\n/**\n * Get the path to skills directory\n */\nexport function getSkillsDir(): string {\n return SKILLS_DIR\n}\n","import { program } from 'commander'\nimport pc from 'picocolors'\nimport { existsSync, mkdirSync, cpSync, readdirSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { getAllSkills, getSkill, getSkillsDir } from './index.js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst SKILLS_SOURCE = join(__dirname, '..', 'skills')\n\nprogram\n .name('recur-skills')\n .description('Claude Code skills for Recur payment integration')\n .version('0.0.1')\n\nprogram\n .command('list')\n .description('List all available skills')\n .action(() => {\n const skills = getAllSkills()\n\n if (skills.length === 0) {\n console.log(pc.yellow('No skills found.'))\n return\n }\n\n console.log(pc.bold('\\nšŸ“¦ Available Recur Skills\\n'))\n\n for (const skill of skills) {\n console.log(pc.cyan(` ${skill.name}`))\n console.log(pc.dim(` ${skill.description}\\n`))\n }\n\n console.log(pc.dim(`Total: ${skills.length} skills\\n`))\n })\n\nprogram\n .command('info <skill>')\n .description('Show detailed information about a skill')\n .action((skillName: string) => {\n const skill = getSkill(skillName)\n\n if (!skill) {\n console.log(pc.red(`Skill \"${skillName}\" not found.`))\n console.log(pc.dim('\\nRun `recur-skills list` to see available skills.'))\n process.exit(1)\n }\n\n console.log(pc.bold(`\\nšŸ“¦ ${skill.name}\\n`))\n console.log(pc.dim('Description:'))\n console.log(` ${skill.description}\\n`)\n console.log(pc.dim('Path:'))\n console.log(` ${skill.path}\\n`)\n })\n\nprogram\n .command('install [skills...]')\n .description('Install skills to your Claude Code skills directory')\n .option('-g, --global', 'Install to global ~/.claude/skills/', false)\n .option('-p, --project', 'Install to project .claude/skills/', false)\n .option('-a, --all', 'Install all skills', false)\n .action((skillNames: string[], options: { global: boolean; project: boolean; all: boolean }) => {\n // Determine target directory\n let targetDir: string\n if (options.global) {\n targetDir = join(process.env.HOME || '~', '.claude', 'skills')\n } else if (options.project) {\n targetDir = join(process.cwd(), '.claude', 'skills')\n } else {\n // Default to global\n targetDir = join(process.env.HOME || '~', '.claude', 'skills')\n }\n\n // Determine which skills to install\n let toInstall: string[]\n if (options.all) {\n toInstall = readdirSync(SKILLS_SOURCE, { withFileTypes: true })\n .filter(d => d.isDirectory())\n .map(d => d.name)\n } else if (skillNames.length === 0) {\n console.log(pc.yellow('Please specify skills to install or use --all'))\n console.log(pc.dim('\\nExamples:'))\n console.log(pc.dim(' recur-skills install recur-quickstart'))\n console.log(pc.dim(' recur-skills install recur-checkout recur-webhooks'))\n console.log(pc.dim(' recur-skills install --all'))\n process.exit(1)\n } else {\n toInstall = skillNames\n }\n\n // Create target directory\n if (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true })\n }\n\n console.log(pc.bold(`\\nšŸ“¦ Installing skills to ${targetDir}\\n`))\n\n let installed = 0\n for (const skillName of toInstall) {\n const sourcePath = join(SKILLS_SOURCE, skillName)\n const destPath = join(targetDir, skillName)\n\n if (!existsSync(sourcePath)) {\n console.log(pc.red(` āœ— ${skillName} - not found`))\n continue\n }\n\n try {\n cpSync(sourcePath, destPath, { recursive: true })\n console.log(pc.green(` āœ“ ${skillName}`))\n installed++\n } catch (error) {\n console.log(pc.red(` āœ— ${skillName} - ${error}`))\n }\n }\n\n console.log(pc.dim(`\\nInstalled ${installed}/${toInstall.length} skills\\n`))\n\n if (installed > 0) {\n console.log(pc.cyan('Skills are now available in Claude Code!'))\n console.log(pc.dim('Claude will automatically use them when relevant,'))\n console.log(pc.dim('or you can invoke them directly with /skill-name\\n'))\n }\n })\n\nprogram\n .command('path')\n .description('Show the path to the skills directory')\n .action(() => {\n console.log(getSkillsDir())\n })\n\nprogram.parse()\n"],"mappings":";;;;;;;;;AAKA,MAAM,aAAa,KADD,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACtB,MAAM,SAAS;;;;AAgBlD,SAAS,iBAAiB,SAAoE;CAC5F,MAAM,QAAQ,QAAQ,MAAM,oCAAoC;AAChE,KAAI,CAAC,MACH,QAAO;EAAE,MAAM,EAAE;EAAE;EAAS;CAG9B,MAAMA,OAA+B,EAAE;CACvC,MAAM,QAAQ,MAAM,GAAG,MAAM,KAAK;AAClC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,KAAK,QAAQ,IAAI;AACpC,MAAI,aAAa,GAAG;GAClB,MAAM,MAAM,KAAK,MAAM,GAAG,WAAW,CAAC,MAAM;AAE5C,QAAK,OADS,KAAK,MAAM,aAAa,EAAE,CAAC,MAAM;;;AAKnD,QAAO;EAAE;EAAM,SAAS,MAAM;EAAI;;;;;AAMpC,SAAgB,eAAwB;AACtC,KAAI,CAAC,WAAW,WAAW,CACzB,QAAO,EAAE;CAGX,MAAM,YAAY,YAAY,YAAY,EAAE,eAAe,MAAM,CAAC,CAC/D,QAAO,WAAU,OAAO,aAAa,CAAC,CACtC,KAAI,WAAU,OAAO,KAAK;CAE7B,MAAMC,SAAkB,EAAE;AAE1B,MAAK,MAAM,OAAO,WAAW;EAC3B,MAAM,YAAY,KAAK,YAAY,KAAK,WAAW;AACnD,MAAI,CAAC,WAAW,UAAU,CAAE;EAG5B,MAAM,EAAE,MAAM,YAAY,iBADP,aAAa,WAAW,QAAQ,CACG;AAEtD,SAAO,KAAK;GACV,MAAM;GACN,MAAM,KAAK,QAAQ;GACnB,aAAa,KAAK,eAAe;GACjC;GACA,MAAM;GACP,CAAC;;AAGJ,QAAO;;;;;AAMT,SAAgB,SAAS,MAA4B;CACnD,MAAM,YAAY,KAAK,YAAY,MAAM,WAAW;AACpD,KAAI,CAAC,WAAW,UAAU,CAAE,QAAO;CAGnC,MAAM,EAAE,MAAM,YAAY,iBADP,aAAa,WAAW,QAAQ,CACG;AAEtD,QAAO;EACL;EACA,MAAM,KAAK,QAAQ;EACnB,aAAa,KAAK,eAAe;EACjC;EACA,MAAM;EACP;;;;;AAoBH,SAAgB,eAAuB;AACrC,QAAO;;;;;ACvGT,MAAM,gBAAgB,KADJ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACnB,MAAM,SAAS;AAErD,QACG,KAAK,eAAe,CACpB,YAAY,mDAAmD,CAC/D,QAAQ,QAAQ;AAEnB,QACG,QAAQ,OAAO,CACf,YAAY,4BAA4B,CACxC,aAAa;CACZ,MAAM,SAAS,cAAc;AAE7B,KAAI,OAAO,WAAW,GAAG;AACvB,UAAQ,IAAI,GAAG,OAAO,mBAAmB,CAAC;AAC1C;;AAGF,SAAQ,IAAI,GAAG,KAAK,gCAAgC,CAAC;AAErD,MAAK,MAAM,SAAS,QAAQ;AAC1B,UAAQ,IAAI,GAAG,KAAK,KAAK,MAAM,OAAO,CAAC;AACvC,UAAQ,IAAI,GAAG,IAAI,QAAQ,MAAM,YAAY,IAAI,CAAC;;AAGpD,SAAQ,IAAI,GAAG,IAAI,UAAU,OAAO,OAAO,WAAW,CAAC;EACvD;AAEJ,QACG,QAAQ,eAAe,CACvB,YAAY,0CAA0C,CACtD,QAAQ,cAAsB;CAC7B,MAAM,QAAQ,SAAS,UAAU;AAEjC,KAAI,CAAC,OAAO;AACV,UAAQ,IAAI,GAAG,IAAI,UAAU,UAAU,cAAc,CAAC;AACtD,UAAQ,IAAI,GAAG,IAAI,qDAAqD,CAAC;AACzE,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,GAAG,KAAK,QAAQ,MAAM,KAAK,IAAI,CAAC;AAC5C,SAAQ,IAAI,GAAG,IAAI,eAAe,CAAC;AACnC,SAAQ,IAAI,KAAK,MAAM,YAAY,IAAI;AACvC,SAAQ,IAAI,GAAG,IAAI,QAAQ,CAAC;AAC5B,SAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;EAChC;AAEJ,QACG,QAAQ,sBAAsB,CAC9B,YAAY,sDAAsD,CAClE,OAAO,gBAAgB,uCAAuC,MAAM,CACpE,OAAO,iBAAiB,sCAAsC,MAAM,CACpE,OAAO,aAAa,sBAAsB,MAAM,CAChD,QAAQ,YAAsB,YAAiE;CAE9F,IAAIC;AACJ,KAAI,QAAQ,OACV,aAAY,KAAK,QAAQ,IAAI,QAAQ,KAAK,WAAW,SAAS;UACrD,QAAQ,QACjB,aAAY,KAAK,QAAQ,KAAK,EAAE,WAAW,SAAS;KAGpD,aAAY,KAAK,QAAQ,IAAI,QAAQ,KAAK,WAAW,SAAS;CAIhE,IAAIC;AACJ,KAAI,QAAQ,IACV,aAAY,YAAY,eAAe,EAAE,eAAe,MAAM,CAAC,CAC5D,QAAO,MAAK,EAAE,aAAa,CAAC,CAC5B,KAAI,MAAK,EAAE,KAAK;UACV,WAAW,WAAW,GAAG;AAClC,UAAQ,IAAI,GAAG,OAAO,gDAAgD,CAAC;AACvE,UAAQ,IAAI,GAAG,IAAI,cAAc,CAAC;AAClC,UAAQ,IAAI,GAAG,IAAI,0CAA0C,CAAC;AAC9D,UAAQ,IAAI,GAAG,IAAI,uDAAuD,CAAC;AAC3E,UAAQ,IAAI,GAAG,IAAI,+BAA+B,CAAC;AACnD,UAAQ,KAAK,EAAE;OAEf,aAAY;AAId,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAG3C,SAAQ,IAAI,GAAG,KAAK,6BAA6B,UAAU,IAAI,CAAC;CAEhE,IAAI,YAAY;AAChB,MAAK,MAAM,aAAa,WAAW;EACjC,MAAM,aAAa,KAAK,eAAe,UAAU;EACjD,MAAM,WAAW,KAAK,WAAW,UAAU;AAE3C,MAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,WAAQ,IAAI,GAAG,IAAI,OAAO,UAAU,cAAc,CAAC;AACnD;;AAGF,MAAI;AACF,UAAO,YAAY,UAAU,EAAE,WAAW,MAAM,CAAC;AACjD,WAAQ,IAAI,GAAG,MAAM,OAAO,YAAY,CAAC;AACzC;WACO,OAAO;AACd,WAAQ,IAAI,GAAG,IAAI,OAAO,UAAU,KAAK,QAAQ,CAAC;;;AAItD,SAAQ,IAAI,GAAG,IAAI,eAAe,UAAU,GAAG,UAAU,OAAO,WAAW,CAAC;AAE5E,KAAI,YAAY,GAAG;AACjB,UAAQ,IAAI,GAAG,KAAK,2CAA2C,CAAC;AAChE,UAAQ,IAAI,GAAG,IAAI,oDAAoD,CAAC;AACxE,UAAQ,IAAI,GAAG,IAAI,qDAAqD,CAAC;;EAE3E;AAEJ,QACG,QAAQ,OAAO,CACf,YAAY,wCAAwC,CACpD,aAAa;AACZ,SAAQ,IAAI,cAAc,CAAC;EAC3B;AAEJ,QAAQ,OAAO"}
1
+ {"version":3,"file":"cli.mjs","names":["data: Record<string, string>","skills: Skill[]","targetDir: string","toInstall: string[]"],"sources":["../src/index.ts","../src/cli.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst SKILLS_DIR = join(__dirname, '..', 'skills')\n\nexport interface SkillMetadata {\n name: string\n description: string\n}\n\nexport interface Skill extends SkillMetadata {\n slug: string\n content: string\n path: string\n}\n\n/**\n * Parse SKILL.md frontmatter\n */\nfunction parseFrontmatter(content: string): { data: Record<string, string>; content: string } {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/)\n if (!match) {\n return { data: {}, content }\n }\n\n const data: Record<string, string> = {}\n const lines = match[1].split('\\n')\n for (const line of lines) {\n const colonIndex = line.indexOf(':')\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim()\n const value = line.slice(colonIndex + 1).trim()\n data[key] = value\n }\n }\n\n return { data, content: match[2] }\n}\n\n/**\n * Get all available skills\n */\nexport function getAllSkills(): Skill[] {\n if (!existsSync(SKILLS_DIR)) {\n return []\n }\n\n const skillDirs = readdirSync(SKILLS_DIR, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory())\n .map(dirent => dirent.name)\n\n const skills: Skill[] = []\n\n for (const dir of skillDirs) {\n const skillPath = join(SKILLS_DIR, dir, 'SKILL.md')\n if (!existsSync(skillPath)) continue\n\n const rawContent = readFileSync(skillPath, 'utf-8')\n const { data, content } = parseFrontmatter(rawContent)\n\n skills.push({\n slug: dir,\n name: data.name || dir,\n description: data.description || '',\n content,\n path: skillPath,\n })\n }\n\n return skills\n}\n\n/**\n * Get a single skill by slug\n */\nexport function getSkill(slug: string): Skill | null {\n const skillPath = join(SKILLS_DIR, slug, 'SKILL.md')\n if (!existsSync(skillPath)) return null\n\n const rawContent = readFileSync(skillPath, 'utf-8')\n const { data, content } = parseFrontmatter(rawContent)\n\n return {\n slug,\n name: data.name || slug,\n description: data.description || '',\n content,\n path: skillPath,\n }\n}\n\n/**\n * Get skill names only (for listing)\n */\nexport function getSkillNames(): string[] {\n if (!existsSync(SKILLS_DIR)) {\n return []\n }\n\n return readdirSync(SKILLS_DIR, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory())\n .filter(dirent => existsSync(join(SKILLS_DIR, dirent.name, 'SKILL.md')))\n .map(dirent => dirent.name)\n}\n\n/**\n * Get the path to skills directory\n */\nexport function getSkillsDir(): string {\n return SKILLS_DIR\n}\n","import { program } from 'commander'\nimport pc from 'picocolors'\nimport { existsSync, mkdirSync, cpSync, readdirSync, readFileSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { getAllSkills, getSkill, getSkillsDir } from './index.js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst SKILLS_SOURCE = join(__dirname, '..', 'skills')\nconst { version } = JSON.parse(\n readFileSync(join(__dirname, '..', 'package.json'), 'utf8')\n) as { version: string }\n\nprogram\n .name('recur-skills')\n .description('Claude Code skills for Recur payment integration')\n .version(version)\n\nprogram\n .command('list')\n .description('List all available skills')\n .action(() => {\n const skills = getAllSkills()\n\n if (skills.length === 0) {\n console.log(pc.yellow('No skills found.'))\n return\n }\n\n console.log(pc.bold('\\nšŸ“¦ Available Recur Skills\\n'))\n\n for (const skill of skills) {\n console.log(pc.cyan(` ${skill.name}`))\n console.log(pc.dim(` ${skill.description}\\n`))\n }\n\n console.log(pc.dim(`Total: ${skills.length} skills\\n`))\n })\n\nprogram\n .command('info <skill>')\n .description('Show detailed information about a skill')\n .action((skillName: string) => {\n const skill = getSkill(skillName)\n\n if (!skill) {\n console.log(pc.red(`Skill \"${skillName}\" not found.`))\n console.log(pc.dim('\\nRun `recur-skills list` to see available skills.'))\n process.exit(1)\n }\n\n console.log(pc.bold(`\\nšŸ“¦ ${skill.name}\\n`))\n console.log(pc.dim('Description:'))\n console.log(` ${skill.description}\\n`)\n console.log(pc.dim('Path:'))\n console.log(` ${skill.path}\\n`)\n })\n\nprogram\n .command('install [skills...]')\n .description('Install skills to your Claude Code skills directory')\n .option('-g, --global', 'Install to global ~/.claude/skills/', false)\n .option('-p, --project', 'Install to project .claude/skills/', false)\n .option('-a, --all', 'Install all skills', false)\n .action((skillNames: string[], options: { global: boolean; project: boolean; all: boolean }) => {\n // Determine target directory\n let targetDir: string\n if (options.global) {\n targetDir = join(process.env.HOME || '~', '.claude', 'skills')\n } else if (options.project) {\n targetDir = join(process.cwd(), '.claude', 'skills')\n } else {\n // Default to global\n targetDir = join(process.env.HOME || '~', '.claude', 'skills')\n }\n\n // Determine which skills to install\n let toInstall: string[]\n if (options.all) {\n toInstall = readdirSync(SKILLS_SOURCE, { withFileTypes: true })\n .filter(d => d.isDirectory())\n .map(d => d.name)\n } else if (skillNames.length === 0) {\n console.log(pc.yellow('Please specify skills to install or use --all'))\n console.log(pc.dim('\\nExamples:'))\n console.log(pc.dim(' recur-skills install recur-quickstart'))\n console.log(pc.dim(' recur-skills install recur-checkout recur-webhooks'))\n console.log(pc.dim(' recur-skills install --all'))\n process.exit(1)\n } else {\n toInstall = skillNames\n }\n\n // Create target directory\n if (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true })\n }\n\n console.log(pc.bold(`\\nšŸ“¦ Installing skills to ${targetDir}\\n`))\n\n let installed = 0\n for (const skillName of toInstall) {\n const sourcePath = join(SKILLS_SOURCE, skillName)\n const destPath = join(targetDir, skillName)\n\n if (!existsSync(sourcePath)) {\n console.log(pc.red(` āœ— ${skillName} - not found`))\n continue\n }\n\n try {\n cpSync(sourcePath, destPath, { recursive: true })\n console.log(pc.green(` āœ“ ${skillName}`))\n installed++\n } catch (error) {\n console.log(pc.red(` āœ— ${skillName} - ${error}`))\n }\n }\n\n console.log(pc.dim(`\\nInstalled ${installed}/${toInstall.length} skills\\n`))\n\n if (installed > 0) {\n console.log(pc.cyan('Skills are now available in Claude Code!'))\n console.log(pc.dim('Claude will automatically use them when relevant,'))\n console.log(pc.dim('or you can invoke them directly with /skill-name\\n'))\n }\n })\n\nprogram\n .command('path')\n .description('Show the path to the skills directory')\n .action(() => {\n console.log(getSkillsDir())\n })\n\nprogram.parse()\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recur-skills",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Claude Code skills for Recur - Taiwan's subscription payment platform",
5
5
  "keywords": [
6
6
  "recur",
@@ -45,6 +45,7 @@
45
45
  "dev": "tsdown --watch",
46
46
  "lint": "eslint src/",
47
47
  "typecheck": "tsc --noEmit",
48
+ "check:examples": "node scripts/typecheck-examples.mjs",
48
49
  "prepublishOnly": "pnpm build",
49
50
  "version": "node scripts/sync-version.js && git add .claude-plugin/marketplace.json skills/*/SKILL.md"
50
51
  },
@@ -53,7 +54,15 @@
53
54
  "picocolors": "^1.1.1"
54
55
  },
55
56
  "devDependencies": {
57
+ "@types/express": "^5.0.6",
56
58
  "@types/node": "^22.0.0",
59
+ "@types/react": "^19.2.17",
60
+ "@types/react-dom": "^19.2.3",
61
+ "express": "^5.2.1",
62
+ "next": "^16.2.10",
63
+ "react": "^19.2.7",
64
+ "react-dom": "^19.2.7",
65
+ "recur-tw": "^0.16.1",
57
66
  "tsdown": "^0.18.4",
58
67
  "typescript": "^5.7.3"
59
68
  },
@@ -1,249 +1,294 @@
1
1
  ---
2
2
  name: recur-checkout
3
- description: Implement Recur checkout flows including embedded, modal, and redirect modes. Use when adding payment buttons, checkout forms, subscription purchase flows, or when user mentions "checkout", "結帳", "ä»˜ę¬¾ęŒ‰éˆ•", "embedded checkout".
3
+ description: Implement Recur checkout flows including hosted, embedded, and modal modes. Use when adding payment buttons, checkout forms, subscription purchase flows, or when user mentions "checkout", "結帳", "ä»˜ę¬¾ęŒ‰éˆ•", "embedded checkout".
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.8"
8
+ verified-against: recur-tw@0.16.1
8
9
  ---
9
10
 
10
11
  # Recur Checkout Integration
11
12
 
12
13
  You are helping implement Recur checkout flows. Recur supports multiple checkout modes for different use cases.
13
14
 
14
- ## Checkout Modes
15
+ ## Choosing a Checkout Mode
15
16
 
16
- | Mode | Best For | User Experience |
17
- |------|----------|-----------------|
18
- | `embedded` | SPA apps | Form renders inline in your page |
19
- | `modal` | Quick purchases | Form appears in a dialog overlay |
20
- | `redirect` | Simple integration | Full page redirect to Recur |
17
+ | Mode | API | Works on localhost | Best for |
18
+ |------|-----|--------------------|----------|
19
+ | **Hosted Checkout** (recommended) | `redirectToCheckout()` | āœ… Yes | Most apps — simplest, works on any domain |
20
+ | On-page (embedded/modal) | `checkout()` | āŒ No — requires a registered domain | Polished production UX on your own domain |
21
+ | Payment Link | `recur.paymentLinks.create()` (server) | āœ… Yes | No-frontend flows: emails, DMs, invoices |
21
22
 
22
- ## Basic Implementation
23
+ **Default to Hosted Checkout.** The on-page `checkout()` renders the card
24
+ form via the PAYUNi SDK and only works on domains registered with Recur —
25
+ it will fail during local development.
23
26
 
24
- ### Using useRecur Hook
27
+ ## Hosted Checkout (recommended)
28
+
29
+ Redirects to `checkout.recur.tw`; the customer pays and is redirected back
30
+ to your `successUrl`.
25
31
 
26
32
  ```tsx
33
+ 'use client'
34
+
27
35
  import { useRecur } from 'recur-tw'
28
36
 
29
- function CheckoutButton({ productId }: { productId: string }) {
30
- const { checkout, isLoading } = useRecur()
37
+ export function SubscribeButton({ productId }: { productId: string }) {
38
+ const { redirectToCheckout, isCheckingOut } = useRecur()
31
39
 
32
40
  const handleClick = async () => {
33
- await checkout({
34
- productId,
35
- // Or use productSlug: 'pro-plan'
36
-
37
- // Optional: Pre-fill customer info
41
+ await redirectToCheckout({
42
+ productId, // or productSlug: 'pro-plan'
43
+ successUrl: `${window.location.origin}/dashboard?welcome=1`,
44
+ cancelUrl: `${window.location.origin}/pricing`,
45
+ // Optional: pre-fill and link to your user system
38
46
  customerEmail: 'user@example.com',
39
- customerName: 'John Doe',
40
-
41
- // Optional: Link to your user system
42
47
  externalCustomerId: 'user_123',
43
-
44
- // Callbacks
45
- onPaymentComplete: (result) => {
46
- console.log('Success!', result)
47
- // result.id - Subscription/Order ID
48
- // result.status - 'ACTIVE', 'TRIALING', etc.
49
- },
50
- onPaymentFailed: (error) => {
51
- console.error('Failed:', error)
52
- return { action: 'retry' } // or 'close' or 'custom'
53
- },
54
- onPaymentCancel: () => {
55
- console.log('User cancelled')
56
- },
57
48
  })
58
49
  }
59
50
 
60
51
  return (
61
- <button onClick={handleClick} disabled={isLoading}>
62
- {isLoading ? 'Processing...' : 'Subscribe'}
52
+ <button onClick={handleClick} disabled={isCheckingOut}>
53
+ {isCheckingOut ? '處理中…' : '訂閱'}
63
54
  </button>
64
55
  )
65
56
  }
66
57
  ```
67
58
 
68
- ### Using useSubscribe Hook (with state management)
59
+ There are no client-side success callbacks in this mode — handle success on
60
+ the `successUrl` page (and authoritatively via webhooks / entitlements).
61
+
62
+ To control the redirect yourself (e.g. open in a new tab):
69
63
 
70
64
  ```tsx
71
- import { useSubscribe } from 'recur-tw'
65
+ 'use client'
66
+
67
+ import { useRecur } from 'recur-tw'
72
68
 
73
- function SubscribeButton({ productId }: { productId: string }) {
74
- const { subscribe, isLoading, error, subscription } = useSubscribe()
69
+ export function OpenCheckoutInNewTab({ productId }: { productId: string }) {
70
+ const { createCheckoutSession } = useRecur()
75
71
 
76
- const handleClick = () => {
77
- subscribe({
72
+ const handleClick = async () => {
73
+ const session = await createCheckoutSession({
78
74
  productId,
79
- onPaymentComplete: (sub) => {
80
- // Subscription created successfully
81
- router.push('/dashboard')
82
- },
75
+ successUrl: `${window.location.origin}/success`,
76
+ cancelUrl: `${window.location.origin}/pricing`,
83
77
  })
78
+ window.open(session.url, '_blank')
84
79
  }
85
80
 
86
- if (subscription) {
87
- return <p>Subscribed! ID: {subscription.id}</p>
81
+ return <button onClick={handleClick}>å‰å¾€ēµåø³</button>
82
+ }
83
+ ```
84
+
85
+ ## On-page Checkout (embedded / modal)
86
+
87
+ āš ļø Requires a domain registered with Recur. Does NOT work on localhost —
88
+ use Hosted Checkout for local development.
89
+
90
+ ```tsx
91
+ 'use client'
92
+
93
+ import { useRecur } from 'recur-tw'
94
+
95
+ export function CheckoutButton({ productId }: { productId: string }) {
96
+ const { checkout, isCheckingOut } = useRecur()
97
+
98
+ const handleClick = async () => {
99
+ await checkout({
100
+ productId, // or productSlug: 'pro-plan'
101
+ customerEmail: 'user@example.com',
102
+ externalCustomerId: 'user_123',
103
+ onPaymentComplete: (subscription) => {
104
+ // subscription.id, subscription.status,
105
+ // subscription.currentPeriodEnd, subscription.trialEndsAt
106
+ console.log('Payment successful!', subscription.id)
107
+ },
108
+ onPaymentFailed: (error) => {
109
+ console.error('Failed:', error.message)
110
+ return { action: 'retry' } // or 'close' or 'custom'
111
+ },
112
+ onPaymentCancel: () => {
113
+ console.log('User cancelled')
114
+ },
115
+ })
88
116
  }
89
117
 
90
118
  return (
91
- <>
92
- <button onClick={handleClick} disabled={isLoading}>
93
- Subscribe
94
- </button>
95
- {error && <p className="error">{error.message}</p>}
96
- </>
119
+ <button onClick={handleClick} disabled={isCheckingOut}>
120
+ {isCheckingOut ? 'Processing...' : 'Subscribe'}
121
+ </button>
97
122
  )
98
123
  }
99
124
  ```
100
125
 
101
- ## Embedded Mode Setup
126
+ ### Embedded mode container
102
127
 
103
- For embedded mode, you need a container element:
128
+ Embedded mode renders the card form into a container element on your page:
104
129
 
105
- ```tsx
130
+ ```tsx no-check
106
131
  // In RecurProvider config
107
132
  <RecurProvider
108
133
  config={{
109
134
  publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY,
110
- checkoutMode: 'embedded',
135
+ checkoutMode: 'embedded', // 'embedded' (default) | 'modal'
111
136
  containerElementId: 'recur-checkout-container',
112
137
  }}
113
138
  >
114
139
  {children}
115
140
  </RecurProvider>
141
+ ```
116
142
 
117
- // In your checkout page
118
- function CheckoutPage() {
143
+ ```tsx
144
+ export function CheckoutPage() {
119
145
  return (
120
146
  <div>
121
147
  <h1>Complete Your Purchase</h1>
122
- {/* Recur will render the payment form here */}
148
+ {/* Recur renders the payment form here */}
123
149
  <div id="recur-checkout-container" />
124
150
  </div>
125
151
  )
126
152
  }
127
153
  ```
128
154
 
129
- ## Handling 3D Verification
155
+ ### Payment failure handling
130
156
 
131
- Recur handles 3D Secure automatically. For mobile apps or specific flows:
157
+ Failure details live in `error.details` (`failure_code`, `can_retry`):
132
158
 
133
159
  ```tsx
134
- await checkout({
135
- productId,
136
- // These URLs are used when 3D verification requires redirect
137
- successUrl: 'https://yourapp.com/checkout/success',
138
- cancelUrl: 'https://yourapp.com/checkout/cancel',
139
- })
160
+ 'use client'
161
+
162
+ import { useRecur, type PaymentFailureDetails } from 'recur-tw'
163
+
164
+ export function CheckoutWithErrorHandling({ productId }: { productId: string }) {
165
+ const { checkout } = useRecur()
166
+
167
+ const handleClick = () =>
168
+ checkout({
169
+ productId,
170
+ onPaymentFailed: (error) => {
171
+ const details = error.details as PaymentFailureDetails | undefined
172
+ switch (details?.failure_code) {
173
+ case 'INSUFFICIENT_FUNDS':
174
+ return {
175
+ action: 'custom' as const,
176
+ customTitle: 'é¤˜é”äøč¶³',
177
+ customMessage: 'č«‹ä½æē”Øå…¶ä»–ä»˜ę¬¾ę–¹å¼',
178
+ }
179
+ case 'NETWORK_ERROR':
180
+ case 'TIMEOUT':
181
+ return { action: 'retry' as const }
182
+ default:
183
+ // undefined = use the SDK's default handling
184
+ return undefined
185
+ }
186
+ },
187
+ })
188
+
189
+ return <button onClick={handleClick}>Subscribe</button>
190
+ }
140
191
  ```
141
192
 
142
- ## Product Types
193
+ ## useSubscribe Hook (with state management)
143
194
 
144
- Recur supports different product types:
195
+ `useSubscribe()` returns `{ subscribe, isLoading, error, reset }` — success
196
+ is reported through the `onPaymentComplete` option, not a return value.
145
197
 
146
198
  ```tsx
147
- // Subscription (recurring)
148
- checkout({ productId: 'prod_subscription_xxx' })
199
+ 'use client'
149
200
 
150
- // One-time purchase
151
- checkout({ productId: 'prod_onetime_xxx' })
201
+ import { useState } from 'react'
202
+ import { useSubscribe, type SubscriptionResult } from 'recur-tw'
152
203
 
153
- // Credits (prepaid wallet)
154
- checkout({ productId: 'prod_credits_xxx' })
204
+ export function SubscribeWithState({ productId }: { productId: string }) {
205
+ const [subscription, setSubscription] = useState<SubscriptionResult | null>(null)
206
+ const { subscribe, isLoading, error } = useSubscribe({
207
+ onPaymentComplete: (sub) => {
208
+ setSubscription(sub)
209
+ },
210
+ })
211
+
212
+ if (subscription) {
213
+ return <p>Subscribed! ID: {subscription.id}</p>
214
+ }
155
215
 
156
- // Donation (variable amount)
157
- checkout({ productId: 'prod_donation_xxx' })
216
+ return (
217
+ <>
218
+ <button onClick={() => subscribe({ productId })} disabled={isLoading}>
219
+ Subscribe
220
+ </button>
221
+ {error && <p className="error">{error.message}</p>}
222
+ </>
223
+ )
224
+ }
158
225
  ```
159
226
 
160
227
  ## Listing Products
161
228
 
229
+ `useProducts()` returns `{ data, isLoading, error, refetch }`. Prices are in
230
+ the smallest currency unit (`29900` = NT$299).
231
+
162
232
  ```tsx
233
+ 'use client'
234
+
163
235
  import { useProducts } from 'recur-tw'
164
236
 
165
- function PricingPage() {
166
- const { products, isLoading } = useProducts({
167
- type: 'SUBSCRIPTION', // Filter by type
237
+ export function PricingPage() {
238
+ const { data: products, isLoading } = useProducts({
239
+ type: 'SUBSCRIPTION', // 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION'
168
240
  })
169
241
 
170
242
  if (isLoading) return <div>Loading...</div>
171
243
 
172
244
  return (
173
245
  <div className="pricing-grid">
174
- {products.map(product => (
175
- <PricingCard key={product.id} product={product} />
246
+ {products?.map((product) => (
247
+ <div key={product.id}>
248
+ <h2>{product.name}</h2>
249
+ <p>
250
+ NT${(product.price / 100).toLocaleString('zh-TW')}
251
+ {product.billingPeriod === 'MONTHLY' && ' / 月'}
252
+ {product.billingPeriod === 'YEARLY' && ' / 幓'}
253
+ </p>
254
+ {product.trialDays ? <p>å…č²»č©¦ē”Ø {product.trialDays} 天</p> : null}
255
+ </div>
176
256
  ))}
177
257
  </div>
178
258
  )
179
259
  }
180
260
  ```
181
261
 
182
- ## Payment Failed Handling
262
+ Product types: `SUBSCRIPTION` (recurring), `ONE_TIME`, `CREDITS` (prepaid
263
+ wallet), `DONATION` (variable amount). All check out the same way.
183
264
 
184
- ```tsx
185
- onPaymentFailed: (error) => {
186
- // error.code tells you what went wrong
187
- switch (error.code) {
188
- case 'CARD_DECLINED':
189
- return { action: 'retry' }
190
- case 'INSUFFICIENT_FUNDS':
191
- return {
192
- action: 'custom',
193
- customTitle: 'é¤˜é”äøč¶³',
194
- customMessage: 'č«‹ä½æē”Øå…¶ä»–ä»˜ę¬¾ę–¹å¼',
195
- }
196
- default:
197
- return { action: 'close' }
198
- }
199
- }
200
- ```
265
+ ## Payment Links (server-side, no frontend needed)
201
266
 
202
- ## Server-Side Checkout (API)
203
-
204
- For server-rendered apps or custom flows:
267
+ Generate a shareable checkout URL from the server:
205
268
 
206
269
  ```typescript
207
- // Create checkout session
208
- const response = await fetch('https://api.recur.tw/v1/checkouts', {
209
- method: 'POST',
210
- headers: {
211
- 'X-Recur-Secret-Key': process.env.RECUR_SECRET_KEY,
212
- 'Content-Type': 'application/json',
213
- },
214
- body: JSON.stringify({
215
- productId: 'prod_xxx',
216
- customerEmail: 'user@example.com',
217
- successUrl: 'https://yourapp.com/success',
218
- cancelUrl: 'https://yourapp.com/cancel',
219
- }),
220
- })
270
+ import { Recur } from 'recur-tw/server'
221
271
 
222
- const { checkoutUrl } = await response.json()
223
- // Redirect user to checkoutUrl
224
- ```
225
-
226
- ## Checkout Result Structure
272
+ const recur = new Recur(process.env.RECUR_SECRET_KEY!)
227
273
 
228
- ```typescript
229
- interface CheckoutResult {
230
- id: string // Subscription or Order ID
231
- status: string // 'ACTIVE', 'TRIALING', 'PENDING'
232
- productId: string
233
- amount: number // In cents (e.g., 29900 = NT$299)
234
- billingPeriod?: string // 'MONTHLY', 'YEARLY' for subscriptions
235
- currentPeriodEnd?: string // ISO date
236
- trialEndsAt?: string // ISO date if trial
274
+ export async function createLink() {
275
+ const link = await recur.paymentLinks.create({
276
+ productSlug: 'pro-plan',
277
+ successUrl: 'https://yourapp.com/success',
278
+ })
279
+ return link.url // share via email, DM, invoice, ...
237
280
  }
238
281
  ```
239
282
 
240
283
  ## Best Practices
241
284
 
242
- 1. **Always handle all callbacks** - onPaymentComplete, onPaymentFailed, onPaymentCancel
243
- 2. **Show loading states** - Use isLoading to disable buttons during checkout
244
- 3. **Pre-fill customer info** - Reduces friction if you already have user data
245
- 4. **Use externalCustomerId** - Links Recur customers to your user system
246
- 5. **Test in sandbox first** - Use `pk_test_` keys during development
285
+ 1. **Default to Hosted Checkout** — on-page `checkout()` fails outside
286
+ registered domains, including localhost.
287
+ 2. **Success is confirmed by the server, not the browser** — treat the
288
+ `successUrl` page as UX only; gate access via webhooks/entitlements.
289
+ 3. **Show loading states** — use `isCheckingOut` to disable buttons.
290
+ 4. **Use `externalCustomerId`** — links Recur customers to your user system.
291
+ 5. **Test in sandbox first** — use `pk_test_` keys during development.
247
292
 
248
293
  ## Related Skills
249
294