recur-skills 0.0.7 → 0.0.9

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.
@@ -1,18 +1,41 @@
1
1
  {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
2
3
  "name": "recur-skills",
4
+ "description": "Claude Code skills for integrating Recur — Taiwan's subscription payment platform",
5
+ "version": "0.0.9",
3
6
  "owner": {
4
7
  "name": "Recur",
5
- "email": "hi@recur.tw"
8
+ "email": "hi@recur.tw",
9
+ "url": "https://recur.tw"
6
10
  },
7
11
  "metadata": {
8
12
  "description": "Claude Code skills for integrating Recur - Taiwan's subscription payment platform",
9
- "version": "0.0.7"
13
+ "version": "0.0.9"
10
14
  },
11
15
  "plugins": [
12
16
  {
13
17
  "name": "recur-skills",
14
- "description": "Skills for Recur SDK integration including quickstart, checkout, webhooks, and entitlements",
18
+ "displayName": "Recur Skills",
19
+ "description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, portal) and the Recur MCP server for signup, API keys, and products",
20
+ "version": "0.0.9",
15
21
  "source": "./",
22
+ "category": "development",
23
+ "homepage": "https://docs.recur.tw/guides/skills",
24
+ "repository": "https://github.com/recur-tw/skills",
25
+ "license": "MIT",
26
+ "keywords": [
27
+ "recur",
28
+ "payments",
29
+ "taiwan",
30
+ "subscription",
31
+ "checkout",
32
+ "webhooks"
33
+ ],
34
+ "author": {
35
+ "name": "Recur",
36
+ "email": "hi@recur.tw",
37
+ "url": "https://recur.tw"
38
+ },
16
39
  "skills": [
17
40
  "./skills/recur-help",
18
41
  "./skills/recur-quickstart",
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "recur-skills",
4
+ "displayName": "Recur Skills",
5
+ "version": "0.0.9",
6
+ "description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, customer portal) plus the Recur MCP server (https://mcp.recur.tw/) for account signup, API keys, and products.",
7
+ "author": {
8
+ "name": "Recur",
9
+ "email": "hi@recur.tw",
10
+ "url": "https://recur.tw"
11
+ },
12
+ "homepage": "https://docs.recur.tw/guides/skills",
13
+ "repository": "https://github.com/recur-tw/skills",
14
+ "license": "MIT",
15
+ "keywords": [
16
+ "recur",
17
+ "payments",
18
+ "taiwan",
19
+ "subscription",
20
+ "checkout",
21
+ "webhooks",
22
+ "mcp"
23
+ ],
24
+ "mcpServers": "./.mcp.json"
25
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "recur": {
4
+ "type": "http",
5
+ "url": "https://mcp.recur.tw/"
6
+ }
7
+ }
8
+ }
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Recur Skills
2
2
 
3
+ [![skills.sh](https://skills.sh/b/recur-tw/skills)](https://skills.sh/recur-tw/skills)
4
+
3
5
  Skills to help developers integrate [Recur](https://recur.tw) - Taiwan's subscription payment platform.
4
6
 
5
7
  Supports Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, Antigravity, and other AI coding agents.
@@ -12,13 +14,32 @@ Supports Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, Antigravity, an
12
14
  npx skills add recur-tw/skills
13
15
  ```
14
16
 
15
- ### Claude Code Plugin
17
+ ### Claude Code Plugin (skills + MCP server)
16
18
 
17
19
  ```bash
18
20
  /plugin marketplace add recur-tw/skills
19
21
  /plugin install recur-skills@recur-skills
20
22
  ```
21
23
 
24
+ The plugin bundles the Recur MCP server (`https://mcp.recur.tw/`, see `.mcp.json`).
25
+ After installing, run `/mcp` and authorize `recur` — no account yet? Click 註冊 on the
26
+ login page; the account is created inside the OAuth flow.
27
+
28
+ ### MCP for other agents
29
+
30
+ `npx skills add` installs skills only. Add `https://mcp.recur.tw/` as a remote MCP
31
+ server in Cursor, VS Code, Codex, Gemini CLI, or Claude Desktop — one-click links
32
+ and config snippets at https://docs.recur.tw/guides/mcp.
33
+
34
+ Or from a shell:
35
+
36
+ ```bash
37
+ claude plugin marketplace add recur-tw/skills
38
+ claude plugin install recur-skills@recur-skills
39
+ ```
40
+
41
+ Plugin skills are namespaced, e.g. `/recur-skills:recur-checkout`. See [Claude Code plugins](https://code.claude.com/docs/en/plugins).
42
+
22
43
  ## Getting Started
23
44
 
24
45
  Not sure where to begin? Ask Claude:
@@ -27,6 +48,12 @@ Not sure where to begin? Ask Claude:
27
48
  Recur 有什麼功能?
28
49
  ```
29
50
 
51
+ No Recur account yet? Ask:
52
+
53
+ ```
54
+ 幫我申請 Recur 帳號,建立每月 $499 的訂閱方案並串接付款流程
55
+ ```
56
+
30
57
  Or type `/recur-help` to see all available skills.
31
58
 
32
59
  ## Available Skills
@@ -98,6 +125,7 @@ Once installed, Claude will automatically use these skills when you're working o
98
125
 
99
126
  ## Links
100
127
 
128
+ - [skills.sh](https://skills.sh/recur-tw/skills): `npx skills add recur-tw/skills`
101
129
  - [Recur Website](https://recur.tw)
102
130
  - [Documentation](https://recur.tw/docs)
103
131
  - [SDK on npm](https://www.npmjs.com/package/recur-tw)
package/dist/cli.mjs CHANGED
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { program } from "commander";
4
3
  import pc from "picocolors";
5
4
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
6
5
  import { dirname, join } from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
-
9
7
  //#region src/index.ts
10
8
  const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
11
9
  /**
@@ -73,11 +71,12 @@ function getSkill(slug) {
73
71
  function getSkillsDir() {
74
72
  return SKILLS_DIR;
75
73
  }
76
-
77
74
  //#endregion
78
75
  //#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");
76
+ const __dirname = dirname(fileURLToPath(import.meta.url));
77
+ const SKILLS_SOURCE = join(__dirname, "..", "skills");
78
+ const { version: PKG_VERSION } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
79
+ program.name("recur-skills").description("Claude Code skills for Recur payment integration").version(PKG_VERSION);
81
80
  program.command("list").description("List all available skills").action(() => {
82
81
  const skills = getAllSkills();
83
82
  if (skills.length === 0) {
@@ -148,7 +147,7 @@ program.command("path").description("Show the path to the skills directory").act
148
147
  console.log(getSkillsDir());
149
148
  });
150
149
  program.parse();
151
-
152
150
  //#endregion
153
- export { };
151
+ export {};
152
+
154
153
  //# sourceMappingURL=cli.mjs.map
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":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"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: PKG_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(PKG_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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";UAOiB,aAAA;EAAA,IAAA,EAAA,MAAA;EAKA,WAAM,EAAA,MAAQ;AAgC/B;AAiCgB,UAjEC,KAAA,SAAc,aAiEc,CAAA;EAmB7B,IAAA,EAAA,MAAA;EAcA,OAAA,EAAA,MAAY;;;;;;iBAlEZ,YAAA,CAAA,GAAgB;;;;iBAiChB,QAAA,gBAAwB;;;;iBAmBxB,aAAA,CAAA;;;;iBAcA,YAAA,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";UAOiB;EACf;EACA;;UAGe,cAAc;EAC7B;EACA;EACA;;;;;iBA6Bc,gBAAgB;;;;iBAiChB,SAAS,eAAe;;;;iBAmBxB;;;;iBAcA"}
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
-
5
4
  //#region src/index.ts
6
5
  const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
7
6
  /**
@@ -76,7 +75,7 @@ function getSkillNames() {
76
75
  function getSkillsDir() {
77
76
  return SKILLS_DIR;
78
77
  }
79
-
80
78
  //#endregion
81
79
  export { getAllSkills, getSkill, getSkillNames, getSkillsDir };
80
+
82
81
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["data: Record<string, string>","skills: Skill[]"],"sources":["../src/index.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"],"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;;;;;AAMH,SAAgB,gBAA0B;AACxC,KAAI,CAAC,WAAW,WAAW,CACzB,QAAO,EAAE;AAGX,QAAO,YAAY,YAAY,EAAE,eAAe,MAAM,CAAC,CACpD,QAAO,WAAU,OAAO,aAAa,CAAC,CACtC,QAAO,WAAU,WAAW,KAAK,YAAY,OAAO,MAAM,WAAW,CAAC,CAAC,CACvE,KAAI,WAAU,OAAO,KAAK;;;;;AAM/B,SAAgB,eAAuB;AACrC,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.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"],"mappings":";;;;AAKA,MAAM,aAAa,KADD,QAAQ,cAAc,OAAO,KAAK,GAAG,CACvB,GAAG,MAAM,QAAQ;;;;AAgBjD,SAAS,iBAAiB,SAAoE;CAC5F,MAAM,QAAQ,QAAQ,MAAM,mCAAmC;CAC/D,IAAI,CAAC,OACH,OAAO;EAAE,MAAM,CAAC;EAAG;CAAQ;CAG7B,MAAM,OAA+B,CAAC;CACtC,MAAM,QAAQ,MAAM,EAAE,CAAC,MAAM,IAAI;CACjC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,KAAK,QAAQ,GAAG;EACnC,IAAI,aAAa,GAAG;GAClB,MAAM,MAAM,KAAK,MAAM,GAAG,UAAU,CAAC,CAAC,KAAK;GAE3C,KAAK,OADS,KAAK,MAAM,aAAa,CAAC,CAAC,CAAC,KACzB;EAClB;CACF;CAEA,OAAO;EAAE;EAAM,SAAS,MAAM;CAAG;AACnC;;;;AAKA,SAAgB,eAAwB;CACtC,IAAI,CAAC,WAAW,UAAU,GACxB,OAAO,CAAC;CAGV,MAAM,YAAY,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC,CAAC,CAC/D,QAAO,WAAU,OAAO,YAAY,CAAC,CAAC,CACtC,KAAI,WAAU,OAAO,IAAI;CAE5B,MAAM,SAAkB,CAAC;CAEzB,KAAK,MAAM,OAAO,WAAW;EAC3B,MAAM,YAAY,KAAK,YAAY,KAAK,UAAU;EAClD,IAAI,CAAC,WAAW,SAAS,GAAG;EAG5B,MAAM,EAAE,MAAM,YAAY,iBADP,aAAa,WAAW,OACS,CAAC;EAErD,OAAO,KAAK;GACV,MAAM;GACN,MAAM,KAAK,QAAQ;GACnB,aAAa,KAAK,eAAe;GACjC;GACA,MAAM;EACR,CAAC;CACH;CAEA,OAAO;AACT;;;;AAKA,SAAgB,SAAS,MAA4B;CACnD,MAAM,YAAY,KAAK,YAAY,MAAM,UAAU;CACnD,IAAI,CAAC,WAAW,SAAS,GAAG,OAAO;CAGnC,MAAM,EAAE,MAAM,YAAY,iBADP,aAAa,WAAW,OACS,CAAC;CAErD,OAAO;EACL;EACA,MAAM,KAAK,QAAQ;EACnB,aAAa,KAAK,eAAe;EACjC;EACA,MAAM;CACR;AACF;;;;AAKA,SAAgB,gBAA0B;CACxC,IAAI,CAAC,WAAW,UAAU,GACxB,OAAO,CAAC;CAGV,OAAO,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC,CAAC,CACpD,QAAO,WAAU,OAAO,YAAY,CAAC,CAAC,CACtC,QAAO,WAAU,WAAW,KAAK,YAAY,OAAO,MAAM,UAAU,CAAC,CAAC,CAAC,CACvE,KAAI,WAAU,OAAO,IAAI;AAC9B;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;AACT"}
package/mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "recur": {
5
+ "type": "streamable-http",
6
+ "url": "https://mcp.recur.tw/"
7
+ }
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "recur-skills",
3
- "version": "0.0.7",
4
- "description": "Claude Code skills for Recur - Taiwan's subscription payment platform",
3
+ "version": "0.0.9",
4
+ "description": "Claude Code skills and MCP plugin for Recur - Taiwan's subscription payment platform",
5
5
  "keywords": [
6
6
  "recur",
7
7
  "claude-code",
@@ -38,29 +38,31 @@
38
38
  "files": [
39
39
  "dist",
40
40
  "skills",
41
- ".claude-plugin"
41
+ ".claude-plugin",
42
+ "plugin.json",
43
+ "mcp.json",
44
+ ".mcp.json"
42
45
  ],
43
- "scripts": {
44
- "build": "tsdown",
45
- "dev": "tsdown --watch",
46
- "lint": "eslint src/",
47
- "typecheck": "tsc --noEmit",
48
- "prepublishOnly": "pnpm build",
49
- "version": "node scripts/sync-version.js && git add .claude-plugin/marketplace.json skills/*/SKILL.md"
50
- },
51
46
  "dependencies": {
52
47
  "commander": "^12.1.0",
53
48
  "picocolors": "^1.1.1"
54
49
  },
55
50
  "devDependencies": {
56
51
  "@types/node": "^22.0.0",
57
- "tsdown": "^0.18.4",
58
- "typescript": "^5.7.3"
52
+ "tsdown": "^0.22.12",
53
+ "typescript": "^6.0.3"
59
54
  },
60
55
  "engines": {
61
56
  "node": ">=18"
62
57
  },
63
58
  "publishConfig": {
64
59
  "access": "public"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
64
+ "lint": "eslint src/",
65
+ "typecheck": "tsc --noEmit",
66
+ "version": "node scripts/sync-version.js && git add .claude-plugin/marketplace.json .claude-plugin/plugin.json plugin.json skills/*/SKILL.md"
65
67
  }
66
- }
68
+ }
package/plugin.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "recur-skills",
4
+ "version": "0.0.9",
5
+ "description": "Skills for Recur SDK integration: quickstart, checkout, webhooks, entitlements, and customer portal. Taiwan subscription billing via PAYUNi.",
6
+ "author": {
7
+ "name": "Recur",
8
+ "email": "hi@recur.tw",
9
+ "url": "https://recur.tw"
10
+ },
11
+ "homepage": "https://docs.recur.tw/guides/skills",
12
+ "repository": "https://github.com/recur-tw/skills",
13
+ "license": "MIT",
14
+ "keywords": [
15
+ "recur",
16
+ "payments",
17
+ "taiwan",
18
+ "subscription",
19
+ "checkout",
20
+ "webhooks",
21
+ "payuni"
22
+ ]
23
+ }
@@ -1,10 +1,10 @@
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 embedded, modal, and redirect modes. Use when adding payment buttons, checkout forms, subscription purchase flows, or when user mentions "checkout", "結帳", "付款按鈕", "embedded checkout". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Checkout Integration
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: recur-entitlements
3
- description: Implement access control and permission checking with Recur entitlements API. Use when building paywalls, checking subscription status, gating premium features, or when user mentions "paywall", "權限檢查", "entitlements", "access control", "premium features".
3
+ description: Implement access control and permission checking with Recur entitlements API. Use when building paywalls, checking subscription status, gating premium features, or when user mentions "paywall", "權限檢查", "entitlements", "access control", "premium features". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Entitlements & Access Control
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: recur-help
3
- description: List all available Recur skills and how to use them. Use when user asks "what can Recur do", "Recur skills", "Recur 有什麼功能", "help with Recur", "如何使用 Recur skills".
3
+ description: List all available Recur skills and how to use them. Use when user asks "what can Recur do", "Recur skills", "Recur 有什麼功能", "help with Recur", "如何使用 Recur skills". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Skills 使用指南
@@ -43,6 +43,12 @@ metadata:
43
43
  - 說:「加上帳戶管理」「customer portal」「訂閱管理」「更新付款方式」
44
44
  - 或輸入:`/recur-portal`
45
45
 
46
+ ## 帳號與 MCP
47
+
48
+ 還沒有 Recur 帳號、API key 或商品時,先用 `/recur-quickstart` 的 Step 0:連上
49
+ Recur MCP(`https://mcp.recur.tw/`,plugin 安裝者直接 `/mcp` 授權),在 OAuth
50
+ 登入頁點「註冊」即可建立帳號,接著用 MCP 工具建立 API key 與商品。
51
+
46
52
  ## 回覆格式
47
53
 
48
54
  用繁體中文回覆,格式如下:
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: recur-portal
3
- description: Implement Customer Portal for subscription self-service. Use when building account pages, letting customers manage subscriptions, update payment methods, view billing history, or when user mentions "customer portal", "帳戶管理", "訂閱管理", "更新付款方式", "self-service".
3
+ description: Implement Customer Portal for subscription self-service. Use when building account pages, letting customers manage subscriptions, update payment methods, view billing history, or when user mentions "customer portal", "帳戶管理", "訂閱管理", "更新付款方式", "self-service". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Customer Portal Integration
@@ -1,16 +1,70 @@
1
1
  ---
2
2
  name: recur-quickstart
3
- description: Quick setup guide for Recur payment integration. Use when starting a new Recur integration, setting up API keys, installing the SDK, or when user mentions "integrate Recur", "setup Recur", "Recur 串接", "金流設定".
3
+ description: Quick setup guide for Recur payment integration, from account signup (via the Recur MCP OAuth flow) to the first checkout. Use when starting a new Recur integration, creating a Recur account, setting up API keys, creating a product, installing the SDK, or when user mentions "申請 Recur 帳號", "integrate Recur", "setup Recur", "Recur 串接", "金流設定". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Quickstart
11
11
 
12
12
  You are helping a developer integrate Recur, Taiwan's subscription payment platform (similar to Stripe Billing).
13
13
 
14
+ ## Step 0: Account, MCP, and First Product
15
+
16
+ Skip this step only if the user already has a Recur API key and a product ID.
17
+
18
+ Recur has no public signup form. **Accounts are created inside the Recur MCP
19
+ OAuth flow**, so make sure the `recur` MCP server is connected. Pick ONE:
20
+
21
+ - **Installed as the `recur-skills` Claude Code plugin** (this skill is
22
+ `/recur-skills:recur-quickstart`): the MCP server is already bundled. Run
23
+ `/mcp`, pick `recur`, authorize. Do NOT run `claude mcp add` — that would
24
+ register a second `recur` server.
25
+ - **Claude Code without the plugin** (skill installed via `npx skills add`):
26
+
27
+ ```bash
28
+ claude mcp add --transport http recur https://mcp.recur.tw/
29
+ ```
30
+
31
+ - **Cursor, VS Code, Codex, Gemini CLI, Claude Desktop**: add
32
+ `https://mcp.recur.tw/` as a remote (streamable HTTP) MCP server. One-click
33
+ links and config snippets: https://docs.recur.tw/guides/mcp
34
+
35
+ Before the first tool call, tell the user what will happen:
36
+
37
+ > A browser window will open. Log in, or click **註冊** (Sign up) on the login
38
+ > page to create a Recur account, then approve the connection. New accounts
39
+ > start in sandbox mode.
40
+
41
+ Then, through MCP:
42
+
43
+ 1. `get_setup_overview` — lists the SANDBOX keys and products that already
44
+ exist. Reuse them; only create what is missing (every create call inserts a
45
+ new row, so rerunning this step blindly makes duplicates).
46
+ 2. `create_api_key` for each SANDBOX key type the overview does NOT show — it
47
+ creates ONE key per call: `type: PUBLISHABLE` and/or `type: SECRET`, with
48
+ `environment: SANDBOX` (`pk_test_…` / `sk_test_…`). A secret key is shown
49
+ only once; store it. An existing secret key cannot be read back — if it was
50
+ lost, create a new one and `revoke_api_key` the old one.
51
+ 3. `create_product` only if the overview / `list_products` has no matching
52
+ product — e.g. name `Pro`, price `499` (TWD integer, NOT cents), interval
53
+ `month` (MCP takes `month` / `year`; only the CLI flag below spells it
54
+ `monthly`), type `SUBSCRIPTION`. Note the returned product `id` (a CUID
55
+ such as `cm5x…`, not a `prod_` prefix).
56
+
57
+ No MCP available? The CLI does the same once the user has a secret key from
58
+ the dashboard (app.recur.tw → 設定 → 開發者):
59
+
60
+ ```bash
61
+ npx @recur-tw/cli login
62
+ recur products create --name "Pro" --price 499 --interval monthly --type SUBSCRIPTION
63
+ ```
64
+
65
+ Charging real cards requires merchant review (apply from app.recur.tw when
66
+ ready); everything else works end-to-end in sandbox.
67
+
14
68
  ## Step 1: Install SDK
15
69
 
16
70
  ```bash
@@ -21,7 +75,7 @@ npm install recur-tw
21
75
 
22
76
  ## Step 2: Get API Keys
23
77
 
24
- API keys are available in the Recur dashboard at `app.recur.tw` → Settings → Developers.
78
+ Use the SANDBOX key pair from Step 0 (MCP `create_api_key`), or copy one from the dashboard at `app.recur.tw` → 設定 → 開發者.
25
79
 
26
80
  **Key formats:**
27
81
  - `pk_test_xxx` - Publishable key (frontend, safe to expose)
@@ -30,8 +84,8 @@ API keys are available in the Recur dashboard at `app.recur.tw` → Settings →
30
84
 
31
85
  **Environment variables to set:**
32
86
  ```bash
33
- RECUR_PUBLISHABLE_KEY=pk_test_xxx
34
- RECUR_SECRET_KEY=sk_test_xxx
87
+ NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY=pk_test_xxx # frontend (Next.js needs the NEXT_PUBLIC_ prefix)
88
+ RECUR_SECRET_KEY=sk_test_xxx # backend only
35
89
  ```
36
90
 
37
91
  ## Step 3: Add Provider (React)
@@ -84,6 +138,7 @@ Create a webhook endpoint to receive payment notifications. See the `recur-webho
84
138
 
85
139
  ## Quick Verification Checklist
86
140
 
141
+ - [ ] Recur account connected via MCP, sandbox API key and product created (Step 0)
87
142
  - [ ] SDK installed (`pnpm list recur-tw`)
88
143
  - [ ] Environment variables set
89
144
  - [ ] RecurProvider wrapping app
@@ -97,8 +152,10 @@ Create a webhook endpoint to receive payment notifications. See the `recur-webho
97
152
  - Ensure using publishable key for frontend, secret key for backend
98
153
 
99
154
  ### "Product not found"
100
- - Verify product exists in Recur dashboard
101
- - Check you're using correct environment (sandbox vs production)
155
+ - Create one first: MCP `create_product`, `recur products create`, or the dashboard
156
+ - Use the real `id` returned by `list_products` / `recur products list` (a CUID like
157
+ `cm5x…`; Recur product IDs have no `prod_` prefix), never a placeholder
158
+ - Check you're using the correct environment (sandbox vs production keys)
102
159
 
103
160
  ### Checkout not appearing
104
161
  - Ensure `RecurProvider` wraps your app
@@ -113,6 +170,8 @@ Create a webhook endpoint to receive payment notifications. See the `recur-webho
113
170
 
114
171
  ## Resources
115
172
 
116
- - [Recur Documentation](https://recur.tw/docs)
173
+ - [Recur Documentation](https://docs.recur.tw)
174
+ - [MCP setup for every agent](https://docs.recur.tw/guides/mcp)
117
175
  - [SDK on npm](https://www.npmjs.com/package/recur-tw)
118
- - [API Reference](https://recur.tw/docs/api)
176
+ - [CLI on npm (`@recur-tw/cli`)](https://www.npmjs.com/package/@recur-tw/cli)
177
+ - [API Reference](https://docs.recur.tw/api)
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: recur-webhooks
3
- description: Set up and handle Recur webhook events for payment notifications. Use when implementing webhook handlers, verifying signatures, handling subscription events, or when user mentions "webhook", "付款通知", "訂閱事件", "payment notification".
3
+ description: Set up and handle Recur webhook events for payment notifications. Use when implementing webhook handlers, verifying signatures, handling subscription events, or when user mentions "webhook", "付款通知", "訂閱事件", "payment notification". Taiwan subscription billing via PAYUNi (recur.tw, 台灣訂閱金流).
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.7"
7
+ version: "0.0.9"
8
8
  ---
9
9
 
10
10
  # Recur Webhook Integration