alpaki-skills 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alpaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # alpaki-skills
2
+
3
+ CLI oficial de [Alpaki](https://alpaki.app) para instalar skills en **Cursor** o **Claude Code**.
4
+
5
+ ```bash
6
+ npx alpaki-skills@latest --skill creative-design/3d-web-experience
7
+ ```
8
+
9
+ ## Qué hace
10
+
11
+ 1. Descarga la skill desde la API de Alpaki (`api.alpaki.app`).
12
+ 2. La guarda como `SKILL.md` en:
13
+ - Cursor: `./.cursor/skills/<slug>/SKILL.md`
14
+ - Claude: `./.claude/skills/<slug>/SKILL.md` (`--tool claude`)
15
+
16
+ No empaqueta las ~800 skills en el npm: se resuelven on-demand, igual que el catálogo web.
17
+
18
+ ## Uso
19
+
20
+ ```bash
21
+ # Instalar una skill (mismo comando que muestra alpaki.app/skills)
22
+ npx alpaki-skills@latest --skill creative-design/3d-web-experience
23
+
24
+ # Atajo sin flag
25
+ npx alpaki-skills@latest development/api-design
26
+
27
+ # Listar / buscar
28
+ npx alpaki-skills@latest list
29
+ npx alpaki-skills@latest search "frontend"
30
+ npx alpaki-skills@latest list --category development
31
+
32
+ # Claude Code
33
+ npx alpaki-skills@latest --skill git/commit --tool claude
34
+
35
+ # Destino custom o global
36
+ npx alpaki-skills@latest --skill utilities/summarize --dir ./skills
37
+ npx alpaki-skills@latest --skill utilities/summarize --global
38
+ ```
39
+
40
+ ## API local / staging
41
+
42
+ ```bash
43
+ ALPAKI_API_BASE=http://localhost:8881 npx alpaki-skills --skill creative-design/3d-web-experience
44
+ # o
45
+ npx alpaki-skills --skill creative-design/3d-web-experience --api http://localhost:8881
46
+ ```
47
+
48
+ ## Publicar en npm
49
+
50
+ ```bash
51
+ npm login
52
+ npm publish --access public
53
+ ```
54
+
55
+ Después de publicar, `npx alpaki-skills@latest` funcionará para cualquiera.
56
+
57
+ ## Desarrollo
58
+
59
+ ```bash
60
+ npm start -- --help
61
+ npm start -- --skill creative-design/3d-web-experience --api http://localhost:8881
62
+ ```
63
+
64
+ ## Licencia
65
+
66
+ MIT. El contenido de cada skill conserva la atribución del origen (claude-code-templates / davila7) cuando aplica.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from '../lib/cli.js'
4
+
5
+ runCli(process.argv.slice(2)).catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error)
7
+ console.error(`\nError: ${message}\n`)
8
+ process.exitCode = 1
9
+ })
package/lib/api.js ADDED
@@ -0,0 +1,108 @@
1
+ const DEFAULT_API_BASE = 'https://api.alpaki.app'
2
+
3
+ /**
4
+ * @param {string} raw
5
+ */
6
+ export function resolveSkillPath(raw) {
7
+ const value = String(raw || '').trim().replace(/^\/+|\/+$/g, '')
8
+ if (!value) {
9
+ throw new Error('Debes indicar --skill categoria/slug (ej. creative-design/3d-web-experience).')
10
+ }
11
+
12
+ const parts = value.split('/').filter(Boolean)
13
+ if (parts.length === 1) {
14
+ return { category: '', slug: parts[0], path: parts[0] }
15
+ }
16
+
17
+ if (parts.length !== 2) {
18
+ throw new Error(`Ruta de skill inválida: "${raw}". Usa categoria/slug.`)
19
+ }
20
+
21
+ return {
22
+ category: parts[0],
23
+ slug: parts[1],
24
+ path: `${parts[0]}/${parts[1]}`,
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {{ category: string, slug: string, path: string }} skillPath
30
+ * @param {{ apiBase?: string }} [options]
31
+ */
32
+ export async function fetchSkill(skillPath, options = {}) {
33
+ const apiBase = normalizeApiBase(options.apiBase)
34
+ const bySlugUrl = `${apiBase}/wp-json/ae/v1/headless/skills/${encodeURIComponent(skillPath.slug)}`
35
+
36
+ const skill = await getJson(bySlugUrl)
37
+ if (!skill?.slug) {
38
+ throw new Error(`No se encontró la skill "${skillPath.path}" en Alpaki.`)
39
+ }
40
+
41
+ if (skillPath.category && skill.sourcePath && skill.sourcePath !== skillPath.path) {
42
+ // Si el usuario pasó categoria/slug, validamos que coincida con la fuente.
43
+ if (!String(skill.sourcePath).endsWith(`/${skillPath.slug}`)) {
44
+ throw new Error(
45
+ `La skill "${skill.slug}" existe, pero no coincide con "${skillPath.path}". Fuente: ${skill.sourcePath}`,
46
+ )
47
+ }
48
+ }
49
+
50
+ return skill
51
+ }
52
+
53
+ /**
54
+ * @param {{ search?: string, category?: string, page?: number, perPage?: number, apiBase?: string }} filters
55
+ */
56
+ export async function fetchSkillsCatalog(filters = {}) {
57
+ const apiBase = normalizeApiBase(filters.apiBase)
58
+ const params = new URLSearchParams()
59
+ if (filters.search) params.set('search', filters.search)
60
+ if (filters.category) params.set('category', filters.category)
61
+ if (filters.page) params.set('page', String(filters.page))
62
+ if (filters.perPage) params.set('perPage', String(filters.perPage))
63
+ const query = params.toString()
64
+ const url = `${apiBase}/wp-json/ae/v1/headless/skills${query ? `?${query}` : ''}`
65
+ return getJson(url)
66
+ }
67
+
68
+ /**
69
+ * @param {string} [apiBase]
70
+ */
71
+ function normalizeApiBase(apiBase) {
72
+ const fromEnv = process.env.ALPAKI_API_BASE || process.env.ALPAPI_API_BASE || ''
73
+ const base = String(apiBase || fromEnv || DEFAULT_API_BASE).replace(/\/$/, '')
74
+ return base
75
+ }
76
+
77
+ /**
78
+ * @param {string} url
79
+ */
80
+ async function getJson(url) {
81
+ let response
82
+ try {
83
+ response = await fetch(url, {
84
+ headers: {
85
+ Accept: 'application/json',
86
+ 'User-Agent': 'alpaki-skills-cli/1.0',
87
+ },
88
+ })
89
+ } catch (error) {
90
+ const reason = error instanceof Error ? error.message : String(error)
91
+ throw new Error(`No se pudo conectar con Alpaki (${url}): ${reason}`)
92
+ }
93
+
94
+ const text = await response.text()
95
+ let data
96
+ try {
97
+ data = text ? JSON.parse(text) : null
98
+ } catch {
99
+ throw new Error(`Respuesta inválida de Alpaki (${response.status}).`)
100
+ }
101
+
102
+ if (!response.ok) {
103
+ const message = data?.message || `HTTP ${response.status}`
104
+ throw new Error(`Alpaki respondió error: ${message}`)
105
+ }
106
+
107
+ return data
108
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,170 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { fetchSkill, fetchSkillsCatalog, resolveSkillPath } from './api.js'
4
+ import { printHelp, printSkillInstalled, printCatalog } from './print.js'
5
+ import { resolveInstallDir } from './paths.js'
6
+
7
+ /**
8
+ * @param {string[]} argv
9
+ */
10
+ export async function runCli(argv) {
11
+ const args = parseArgs(argv)
12
+
13
+ if (args.help || (!args.skill && !args.list && !args.search)) {
14
+ printHelp()
15
+ if (!args.help && !args.skill && !args.list && !args.search) {
16
+ process.exitCode = 1
17
+ }
18
+ return
19
+ }
20
+
21
+ if (args.list || args.search) {
22
+ const catalog = await fetchSkillsCatalog({
23
+ search: args.search || '',
24
+ category: args.category || '',
25
+ page: args.page,
26
+ perPage: args.perPage,
27
+ apiBase: args.api,
28
+ })
29
+ printCatalog(catalog, { search: args.search || '' })
30
+ return
31
+ }
32
+
33
+ const skillPath = resolveSkillPath(args.skill)
34
+ const skill = await fetchSkill(skillPath, { apiBase: args.api })
35
+ const installRoot = resolveInstallDir({
36
+ cwd: process.cwd(),
37
+ target: args.dir,
38
+ global: args.global,
39
+ tool: args.tool,
40
+ })
41
+
42
+ const folderName = skill.slug || skillPath.slug
43
+ const skillDir = path.join(installRoot, folderName)
44
+ const skillFile = path.join(skillDir, 'SKILL.md')
45
+ const body = buildSkillMarkdown(skill)
46
+
47
+ await mkdir(skillDir, { recursive: true })
48
+ await writeFile(skillFile, body, 'utf8')
49
+
50
+ printSkillInstalled({
51
+ skill,
52
+ skillPath,
53
+ skillFile,
54
+ installRoot,
55
+ })
56
+ }
57
+
58
+ /**
59
+ * @param {string[]} argv
60
+ */
61
+ function parseArgs(argv) {
62
+ /** @type {Record<string, any>} */
63
+ const out = {
64
+ help: false,
65
+ list: false,
66
+ global: false,
67
+ skill: '',
68
+ search: '',
69
+ category: '',
70
+ dir: '',
71
+ tool: 'cursor',
72
+ api: '',
73
+ page: 1,
74
+ perPage: 24,
75
+ }
76
+
77
+ for (let i = 0; i < argv.length; i += 1) {
78
+ const token = argv[i]
79
+ const next = argv[i + 1]
80
+
81
+ if (token === '-h' || token === '--help') {
82
+ out.help = true
83
+ continue
84
+ }
85
+ if (token === 'list' || token === '--list') {
86
+ out.list = true
87
+ continue
88
+ }
89
+ if (token === 'search' || token === '--search') {
90
+ out.search = next || ''
91
+ i += 1
92
+ continue
93
+ }
94
+ if (token === '--skill' || token === '-s') {
95
+ out.skill = next || ''
96
+ i += 1
97
+ continue
98
+ }
99
+ if (token === '--dir' || token === '-d') {
100
+ out.dir = next || ''
101
+ i += 1
102
+ continue
103
+ }
104
+ if (token === '--tool' || token === '-t') {
105
+ out.tool = next || 'cursor'
106
+ i += 1
107
+ continue
108
+ }
109
+ if (token === '--api') {
110
+ out.api = next || ''
111
+ i += 1
112
+ continue
113
+ }
114
+ if (token === '--category' || token === '-c') {
115
+ out.category = next || ''
116
+ i += 1
117
+ continue
118
+ }
119
+ if (token === '--page') {
120
+ out.page = Number(next) || 1
121
+ i += 1
122
+ continue
123
+ }
124
+ if (token === '--per-page') {
125
+ out.perPage = Number(next) || 24
126
+ i += 1
127
+ continue
128
+ }
129
+ if (token === '--global' || token === '-g') {
130
+ out.global = true
131
+ continue
132
+ }
133
+ if (!token.startsWith('-') && !out.skill && token.includes('/')) {
134
+ out.skill = token
135
+ continue
136
+ }
137
+ }
138
+
139
+ return out
140
+ }
141
+
142
+ /**
143
+ * @param {{ title?: string, slug?: string, excerpt?: string, content?: string, sourceName?: string, category?: { name?: string, slug?: string } | null, license?: string }} skill
144
+ */
145
+ function buildSkillMarkdown(skill) {
146
+ const name = skill.sourceName || skill.slug || skill.title || 'skill'
147
+ const description = (skill.excerpt || skill.title || name).replace(/\s+/g, ' ').trim()
148
+ const lines = [
149
+ '---',
150
+ `name: ${yamlQuote(name)}`,
151
+ `description: ${yamlQuote(description)}`,
152
+ ]
153
+
154
+ if (skill.license) {
155
+ lines.push(`license: ${yamlQuote(skill.license)}`)
156
+ }
157
+
158
+ lines.push('---', '')
159
+ lines.push((skill.content || '').trim(), '')
160
+
161
+ return `${lines.join('\n').trim()}\n`
162
+ }
163
+
164
+ /**
165
+ * @param {string} value
166
+ */
167
+ function yamlQuote(value) {
168
+ const safe = String(value).replace(/"/g, '\\"')
169
+ return `"${safe}"`
170
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,23 @@
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * @param {{ cwd: string, target?: string, global?: boolean, tool?: string }} options
6
+ */
7
+ export function resolveInstallDir(options) {
8
+ if (options.target) {
9
+ return path.resolve(options.cwd, options.target)
10
+ }
11
+
12
+ const tool = String(options.tool || 'cursor').toLowerCase()
13
+ const relative =
14
+ tool === 'claude' || tool === 'claude-code'
15
+ ? path.join('.claude', 'skills')
16
+ : path.join('.cursor', 'skills')
17
+
18
+ if (options.global) {
19
+ return path.join(os.homedir(), relative)
20
+ }
21
+
22
+ return path.join(options.cwd, relative)
23
+ }
package/lib/print.js ADDED
@@ -0,0 +1,78 @@
1
+ export function printHelp() {
2
+ console.log(`
3
+ alpaki-skills — instala skills de Alpaki en Cursor / Claude Code
4
+
5
+ Uso:
6
+ npx alpaki-skills@latest --skill <categoria/slug>
7
+ npx alpaki-skills@latest creative-design/3d-web-experience
8
+ npx alpaki-skills@latest list
9
+ npx alpaki-skills@latest search "frontend"
10
+
11
+ Opciones:
12
+ --skill, -s Ruta categoria/slug (ej. development/api-design)
13
+ --dir, -d Carpeta destino (default: ./.cursor/skills)
14
+ --tool, -t cursor | claude (default: cursor)
15
+ --global, -g Instalar en el home del usuario
16
+ --api Base URL de la API (default: https://api.alpaki.app)
17
+ --category, -c Filtrar catálogo por categoría
18
+ --page Página del catálogo
19
+ --per-page Items por página
20
+ --help, -h Mostrar ayuda
21
+
22
+ Ejemplos:
23
+ npx alpaki-skills@latest --skill creative-design/3d-web-experience
24
+ npx alpaki-skills@latest --skill git/commit --tool claude
25
+ npx alpaki-skills@latest list --category development
26
+ `)
27
+ }
28
+
29
+ /**
30
+ * @param {{ skill: any, skillPath: { path: string }, skillFile: string, installRoot: string }} payload
31
+ */
32
+ export function printSkillInstalled(payload) {
33
+ const { skill, skillPath, skillFile, installRoot } = payload
34
+ console.log(`
35
+ ✓ Skill instalada
36
+
37
+ ${skill.title || skill.slug}
38
+ Origen: ${skillPath.path}
39
+ Destino: ${skillFile}
40
+ Carpeta: ${installRoot}
41
+
42
+ Ábrela en Cursor como skill local o cópiala a tu flujo de agente.
43
+ Catálogo: https://alpaki.app/skills
44
+ `)
45
+ }
46
+
47
+ /**
48
+ * @param {{ items?: any[], total?: number, page?: number, totalPages?: number, categories?: any[] }} catalog
49
+ * @param {{ search?: string }} [meta]
50
+ */
51
+ export function printCatalog(catalog, meta = {}) {
52
+ const items = catalog.items || []
53
+ const total = catalog.total ?? items.length
54
+ const header = meta.search ? `Resultados para "${meta.search}"` : 'Catálogo Alpaki Skills'
55
+
56
+ console.log(`\n${header} (${total})\n`)
57
+
58
+ if (!items.length) {
59
+ console.log('No hay skills con ese filtro.\n')
60
+ return
61
+ }
62
+
63
+ for (const item of items) {
64
+ const pathLabel = item.sourcePath || item.slug
65
+ const category = item.category?.name || 'Skill'
66
+ console.log(`- ${pathLabel}`)
67
+ console.log(` ${item.title} · ${category}`)
68
+ if (item.excerpt) {
69
+ const excerpt = String(item.excerpt).replace(/\s+/g, ' ').slice(0, 110)
70
+ console.log(` ${excerpt}${item.excerpt.length > 110 ? '…' : ''}`)
71
+ }
72
+ console.log('')
73
+ }
74
+
75
+ if ((catalog.totalPages || 1) > 1) {
76
+ console.log(`Página ${catalog.page || 1} de ${catalog.totalPages}. Usa --page para ver más.\n`)
77
+ }
78
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "alpaki-skills",
3
+ "version": "1.0.0",
4
+ "description": "CLI para instalar skills de Alpaki en Cursor / Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "alpaki-skills": "bin/alpaki-skills.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node ./bin/alpaki-skills.js",
17
+ "test": "node --test test/*.test.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "alpaki",
24
+ "skills",
25
+ "cursor",
26
+ "claude",
27
+ "cli"
28
+ ],
29
+ "author": "Alpaki",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/danielandrehz/alpaki-skills.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/danielandrehz/alpaki-skills/issues"
37
+ },
38
+ "homepage": "https://alpaki.app/skills"
39
+ }