openclaw-skills-cli 0.1.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/PUBLISH-INSTRUCTIONS.md +209 -0
- package/README.md +76 -0
- package/dist/api/client.js +115 -0
- package/dist/cli.js +28 -0
- package/dist/commands/import.js +169 -0
- package/dist/commands/install.js +272 -0
- package/dist/commands/login.js +27 -0
- package/dist/commands/publish.js +222 -0
- package/dist/commands/search.js +71 -0
- package/dist/config.js +28 -0
- package/dist/utils/errors.js +37 -0
- package/package.json +49 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# 📦 Instrucciones para Publicar CLI en npm
|
|
2
|
+
|
|
3
|
+
## ✅ Preparación Completada
|
|
4
|
+
|
|
5
|
+
- ✅ Package renombrado a `openclaw-skills-cli` (sin scope)
|
|
6
|
+
- ✅ README.md creado para página npm
|
|
7
|
+
- ✅ .npmignore configurado
|
|
8
|
+
- ✅ CLI compilado y listo
|
|
9
|
+
- ✅ Todas las referencias actualizadas en el código
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🚀 Pasos para Publicar (José)
|
|
14
|
+
|
|
15
|
+
### **Paso 1: Crear Cuenta npm** (5 minutos)
|
|
16
|
+
|
|
17
|
+
1. Ve a https://www.npmjs.com/signup
|
|
18
|
+
2. Rellena el formulario:
|
|
19
|
+
- **Username:** `jotajota1302` (o el que prefieras)
|
|
20
|
+
- **Email:** Tu email
|
|
21
|
+
- **Password:** Contraseña segura
|
|
22
|
+
3. Verifica tu email (te llegará un correo de confirmación)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### **Paso 2: Login desde la Terminal** (1 minuto)
|
|
27
|
+
|
|
28
|
+
Abre una terminal y ejecuta:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm login
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Te pedirá:
|
|
35
|
+
- **Username:** (el que elegiste)
|
|
36
|
+
- **Password:** (tu contraseña)
|
|
37
|
+
- **Email:** (tu email)
|
|
38
|
+
|
|
39
|
+
**Nota:** Si te pide autenticación web, sigue el link que te muestre.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### **Paso 3: Verificar que Estás Logueado**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm whoami
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Debería mostrar tu username. Si muestra error, repite `npm login`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### **Paso 4: Publicar el CLI** (30 segundos)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd ~/Desktop/PROYECTOS/activos/openclaw-skills-registry/cli
|
|
57
|
+
npm publish
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Resultado esperado:**
|
|
61
|
+
```
|
|
62
|
+
+ openclaw-skills-cli@0.1.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Si sale error "name already taken":**
|
|
66
|
+
```bash
|
|
67
|
+
# Edita package.json y cambia el nombre
|
|
68
|
+
nano package.json
|
|
69
|
+
# Cambia "openclaw-skills-cli" por "openclaw-skills" o "skills-cli-openclaw"
|
|
70
|
+
# Luego repite: npm publish
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### **Paso 5: Verificar Publicación** (1 minuto)
|
|
76
|
+
|
|
77
|
+
Abre una nueva terminal (sin el proyecto) y prueba:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx openclaw-skills-cli@latest --version
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Debería mostrar: `0.1.0`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### **Paso 6: Test Completo**
|
|
88
|
+
|
|
89
|
+
Prueba instalar una skill:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx openclaw-skills-cli search security
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Debería mostrar resultados de skills de seguridad.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## ✅ Después de Publicar
|
|
100
|
+
|
|
101
|
+
Una vez publicado, **TODOS los comandos en /clients funcionarán:**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Esto ya funcionará:
|
|
105
|
+
npx openclaw-skills-cli install skillia/ai-governance-audit
|
|
106
|
+
|
|
107
|
+
# Y esto también:
|
|
108
|
+
npm i -g openclaw-skills-cli
|
|
109
|
+
openclaw-skills install skill/name
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🎯 Verificación Final
|
|
115
|
+
|
|
116
|
+
1. Ve a: https://www.npmjs.com/package/openclaw-skills-cli
|
|
117
|
+
2. Deberías ver tu paquete publicado con README, versión, stats, etc.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 📊 Métricas npm
|
|
122
|
+
|
|
123
|
+
Después de publicar, podrás ver en npm:
|
|
124
|
+
- Descargas semanales
|
|
125
|
+
- Dependents (quién usa tu paquete)
|
|
126
|
+
- GitHub stars
|
|
127
|
+
- Versiones publicadas
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🔄 Publicar Actualizaciones Futuras
|
|
132
|
+
|
|
133
|
+
Cuando quieras publicar una nueva versión:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd cli
|
|
137
|
+
|
|
138
|
+
# Opción A: Patch (0.1.0 → 0.1.1)
|
|
139
|
+
npm version patch
|
|
140
|
+
|
|
141
|
+
# Opción B: Minor (0.1.0 → 0.2.0)
|
|
142
|
+
npm version minor
|
|
143
|
+
|
|
144
|
+
# Opción C: Major (0.1.0 → 1.0.0)
|
|
145
|
+
npm version major
|
|
146
|
+
|
|
147
|
+
# Publicar
|
|
148
|
+
npm publish
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## ⚠️ Troubleshooting
|
|
154
|
+
|
|
155
|
+
### Error: "You must be logged in"
|
|
156
|
+
```bash
|
|
157
|
+
npm logout
|
|
158
|
+
npm login
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Error: "402 Payment Required"
|
|
162
|
+
Solo para scoped packages (@org/pkg). Nosotros usamos `openclaw-skills-cli` (sin scope), así que NO deberías ver este error.
|
|
163
|
+
|
|
164
|
+
### Error: "403 Forbidden"
|
|
165
|
+
```bash
|
|
166
|
+
# Verifica que estás logueado:
|
|
167
|
+
npm whoami
|
|
168
|
+
|
|
169
|
+
# Si no muestra tu usuario:
|
|
170
|
+
npm login
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Error: "name already taken"
|
|
174
|
+
Cambia el nombre en `package.json` y repite:
|
|
175
|
+
```bash
|
|
176
|
+
# Opciones alternativas:
|
|
177
|
+
"name": "openclaw-skills"
|
|
178
|
+
"name": "skills-cli-openclaw"
|
|
179
|
+
"name": "openclaw-registry-cli"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 📞 Si Tienes Problemas
|
|
185
|
+
|
|
186
|
+
Avísame por Telegram y te ayudo en tiempo real.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🎉 Resultado Esperado
|
|
191
|
+
|
|
192
|
+
**Antes:**
|
|
193
|
+
```bash
|
|
194
|
+
$ npx openclaw-skills-cli install skill
|
|
195
|
+
→ 404 Not Found
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Después:**
|
|
199
|
+
```bash
|
|
200
|
+
$ npx openclaw-skills-cli install skill
|
|
201
|
+
→ ✓ Installing skill...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Clientes podrán instalar skills sin problemas** ✅
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
**Preparado por:** JARVIS
|
|
209
|
+
**Fecha:** 2026-02-27 18:05 GMT+1
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# openclaw-skills-cli
|
|
2
|
+
|
|
3
|
+
Official CLI for [Skillia](https://openclaw-skills-registry-dashboard.vercel.app) - The OpenClaw Skills Registry.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Quick Run (npx)
|
|
8
|
+
```bash
|
|
9
|
+
npx openclaw-skills-cli install namespace/skill-name
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Global Install
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g openclaw-skills-cli
|
|
15
|
+
openclaw-skills --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Search Skills
|
|
21
|
+
```bash
|
|
22
|
+
openclaw-skills search security
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Install a Skill
|
|
26
|
+
```bash
|
|
27
|
+
# Free skills
|
|
28
|
+
openclaw-skills install skillia/ai-governance-audit
|
|
29
|
+
|
|
30
|
+
# Paid skills (requires license token)
|
|
31
|
+
openclaw-skills install --license-token <token> namespace/skill-name
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Publish a Skill
|
|
35
|
+
|
|
36
|
+
1. Create an API token at [Skillia Account](https://openclaw-skills-registry-dashboard.vercel.app/account)
|
|
37
|
+
2. Login with your token:
|
|
38
|
+
```bash
|
|
39
|
+
openclaw-skills login <your-api-token>
|
|
40
|
+
```
|
|
41
|
+
3. Publish your skill:
|
|
42
|
+
```bash
|
|
43
|
+
cd my-skill-folder
|
|
44
|
+
openclaw-skills publish
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
- `login <token>` - Authenticate with your API token
|
|
50
|
+
- `search [query]` - Search for skills in the registry
|
|
51
|
+
- `install <namespace/name>` - Install a skill
|
|
52
|
+
- `publish [folder]` - Publish a skill to the registry
|
|
53
|
+
- `import` - Import MCP servers from Anthropic registry
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
The CLI stores configuration in `~/.config/openclaw-skills/config.json`.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
Default API URL: `https://openclaw-skills-registry.onrender.com`
|
|
62
|
+
|
|
63
|
+
Override with `--api-url` flag:
|
|
64
|
+
```bash
|
|
65
|
+
openclaw-skills --api-url http://localhost:3000 search test
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
71
|
+
|
|
72
|
+
## Links
|
|
73
|
+
|
|
74
|
+
- [Skillia Marketplace](https://openclaw-skills-registry-dashboard.vercel.app)
|
|
75
|
+
- [Documentation](https://openclaw-skills-registry-dashboard.vercel.app/developers)
|
|
76
|
+
- [GitHub](https://github.com/jotajota1302/skills-registry)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createToken = createToken;
|
|
7
|
+
exports.listSkills = listSkills;
|
|
8
|
+
exports.getSkill = getSkill;
|
|
9
|
+
exports.publishSkill = publishSkill;
|
|
10
|
+
exports.getInstallConfig = getInstallConfig;
|
|
11
|
+
exports.getVersion = getVersion;
|
|
12
|
+
exports.listVersions = listVersions;
|
|
13
|
+
exports.publishVersion = publishVersion;
|
|
14
|
+
exports.registerInstall = registerInstall;
|
|
15
|
+
exports.triggerMcpImport = triggerMcpImport;
|
|
16
|
+
exports.checkHealth = checkHealth;
|
|
17
|
+
exports.getInstallPack = getInstallPack;
|
|
18
|
+
exports.consumeInstallPack = consumeInstallPack;
|
|
19
|
+
const axios_1 = __importDefault(require("axios"));
|
|
20
|
+
const config_js_1 = require("../config.js");
|
|
21
|
+
function createClient() {
|
|
22
|
+
const client = axios_1.default.create({
|
|
23
|
+
baseURL: (0, config_js_1.getApiUrl)(),
|
|
24
|
+
timeout: 15000,
|
|
25
|
+
});
|
|
26
|
+
client.interceptors.request.use((config) => {
|
|
27
|
+
const token = (0, config_js_1.getToken)();
|
|
28
|
+
if (token) {
|
|
29
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
});
|
|
33
|
+
return client;
|
|
34
|
+
}
|
|
35
|
+
async function createToken(clientId, secret) {
|
|
36
|
+
const client = createClient();
|
|
37
|
+
const { data } = await client.post('/v1/auth/token', { clientId, secret });
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
async function listSkills(opts) {
|
|
41
|
+
const client = createClient();
|
|
42
|
+
const params = {
|
|
43
|
+
q: opts.q,
|
|
44
|
+
keyword: opts.keyword,
|
|
45
|
+
page: opts.page,
|
|
46
|
+
limit: opts.limit,
|
|
47
|
+
sort: opts.sort,
|
|
48
|
+
type: opts.type,
|
|
49
|
+
tier: opts.tier,
|
|
50
|
+
};
|
|
51
|
+
if (opts.certified) {
|
|
52
|
+
params.certified = 'true';
|
|
53
|
+
}
|
|
54
|
+
const { data } = await client.get('/v1/skills', { params });
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
async function getSkill(namespace, name) {
|
|
58
|
+
const client = createClient();
|
|
59
|
+
const { data } = await client.get(`/v1/skills/${namespace}/${name}`);
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
async function publishSkill(skillData) {
|
|
63
|
+
const client = createClient();
|
|
64
|
+
const { data } = await client.post('/v1/skills', skillData);
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
async function getInstallConfig(namespace, name) {
|
|
68
|
+
const client = createClient();
|
|
69
|
+
const { data } = await client.get(`/v1/skills/${namespace}/${name}/install-config`);
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
async function getVersion(namespace, name, version) {
|
|
73
|
+
const client = createClient();
|
|
74
|
+
const { data } = await client.get(`/v1/skills/${namespace}/${name}/${version}`);
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
async function listVersions(namespace, name) {
|
|
78
|
+
const client = createClient();
|
|
79
|
+
const { data } = await client.get(`/v1/skills/${namespace}/${name}/versions`);
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
async function publishVersion(namespace, name, versionData) {
|
|
83
|
+
const client = createClient();
|
|
84
|
+
const { data } = await client.post(`/v1/skills/${namespace}/${name}/versions`, versionData);
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
async function registerInstall(namespace, name, version) {
|
|
88
|
+
const client = createClient();
|
|
89
|
+
const { data } = await client.post(`/v1/skills/${namespace}/${name}/installs`, version ? { version } : {});
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
async function triggerMcpImport(opts) {
|
|
93
|
+
const client = createClient();
|
|
94
|
+
const params = {
|
|
95
|
+
dryRun: opts.dryRun ? 'true' : undefined,
|
|
96
|
+
limit: opts.limit,
|
|
97
|
+
};
|
|
98
|
+
const { data } = await client.post('/v1/admin/import/mcp', undefined, { params });
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
async function checkHealth() {
|
|
102
|
+
const client = createClient();
|
|
103
|
+
const { data } = await client.get('/v1/health');
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
async function getInstallPack(token) {
|
|
107
|
+
const client = createClient();
|
|
108
|
+
const { data } = await client.get(`/v1/install-packs/${token}`);
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
async function consumeInstallPack(token) {
|
|
112
|
+
const client = createClient();
|
|
113
|
+
const { data } = await client.post(`/v1/install-packs/${token}/consume`);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const config_js_1 = require("./config.js");
|
|
6
|
+
const login_js_1 = require("./commands/login.js");
|
|
7
|
+
const search_js_1 = require("./commands/search.js");
|
|
8
|
+
const install_js_1 = require("./commands/install.js");
|
|
9
|
+
const publish_js_1 = require("./commands/publish.js");
|
|
10
|
+
const import_js_1 = require("./commands/import.js");
|
|
11
|
+
const program = new commander_1.Command();
|
|
12
|
+
program
|
|
13
|
+
.name('openclaw-skills')
|
|
14
|
+
.description('CLI del OpenClaw Skills Registry')
|
|
15
|
+
.version('0.1.0')
|
|
16
|
+
.option('--api-url <url>', 'URL de la API del registry')
|
|
17
|
+
.hook('preAction', (thisCommand) => {
|
|
18
|
+
const opts = thisCommand.opts();
|
|
19
|
+
if (opts.apiUrl) {
|
|
20
|
+
(0, config_js_1.setApiUrl)(opts.apiUrl);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
program.addCommand(login_js_1.loginCommand);
|
|
24
|
+
program.addCommand(search_js_1.searchCommand);
|
|
25
|
+
program.addCommand(install_js_1.installCommand);
|
|
26
|
+
program.addCommand(publish_js_1.publishCommand);
|
|
27
|
+
program.addCommand(import_js_1.importCommand);
|
|
28
|
+
program.parse();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.importCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const axios_1 = __importDefault(require("axios"));
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const client_js_1 = require("../api/client.js");
|
|
47
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
48
|
+
const config_js_1 = require("../config.js");
|
|
49
|
+
const EXCLUDED_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', 'coverage']);
|
|
50
|
+
function readFilesRecursive(dir, baseDir) {
|
|
51
|
+
const files = {};
|
|
52
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (EXCLUDED_DIRS.has(entry.name))
|
|
55
|
+
continue;
|
|
56
|
+
if (entry.name.startsWith('.env'))
|
|
57
|
+
continue;
|
|
58
|
+
const fullPath = path.join(dir, entry.name);
|
|
59
|
+
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
Object.assign(files, readFilesRecursive(fullPath, baseDir));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
files[relativePath] = fs.readFileSync(fullPath, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore binary/unreadable files
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
const mcpImportCommand = new commander_1.Command('mcp')
|
|
74
|
+
.description('Importar servidores MCP desde el registry oficial')
|
|
75
|
+
.option('--dry-run', 'Simula import sin escribir en DB')
|
|
76
|
+
.option('--limit <n>', 'Limita número de registros procesados')
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const spinner = (0, ora_1.default)('Importando MCP registry...').start();
|
|
79
|
+
try {
|
|
80
|
+
const result = await (0, client_js_1.triggerMcpImport)({
|
|
81
|
+
dryRun: !!opts.dryRun,
|
|
82
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
83
|
+
});
|
|
84
|
+
spinner.succeed('Import completado');
|
|
85
|
+
const stats = result?.result || {};
|
|
86
|
+
console.log(chalk_1.default.bold('\nMCP Import Result'));
|
|
87
|
+
console.log(`dryRun: ${result?.dryRun ? 'true' : 'false'}`);
|
|
88
|
+
console.log(`scanned: ${stats.scanned ?? 0}`);
|
|
89
|
+
console.log(`imported: ${stats.imported ?? 0}`);
|
|
90
|
+
console.log(`updated: ${stats.updated ?? 0}`);
|
|
91
|
+
console.log(`skipped: ${stats.skipped ?? 0}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
spinner.fail('Error importando MCP registry');
|
|
95
|
+
(0, errors_js_1.handleApiError)(error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const domainImportCommand = new commander_1.Command('domain')
|
|
99
|
+
.description('Publicar una domain skill local en el registry (no interactivo)')
|
|
100
|
+
.argument('<folder>', 'Carpeta local de la skill')
|
|
101
|
+
.option('--namespace <namespace>', 'Override namespace')
|
|
102
|
+
.option('--name <name>', 'Override nombre de skill')
|
|
103
|
+
.option('--version <version>', 'Override versión')
|
|
104
|
+
.option('--tier <tier>', 'Tier: free|pro|enterprise')
|
|
105
|
+
.option('--description <description>', 'Override descripción')
|
|
106
|
+
.action(async (folder, opts) => {
|
|
107
|
+
const token = (0, config_js_1.getToken)();
|
|
108
|
+
if (!token) {
|
|
109
|
+
console.log(chalk_1.default.red("Ejecuta 'openclaw-skills login <token>' primero"));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const resolvedFolder = path.resolve(folder);
|
|
114
|
+
const skillJsonPath = path.join(resolvedFolder, 'skill.json');
|
|
115
|
+
const skillMdPath = path.join(resolvedFolder, 'SKILL.md');
|
|
116
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
117
|
+
throw new Error(`No se encontró SKILL.md en ${resolvedFolder}`);
|
|
118
|
+
}
|
|
119
|
+
if (!fs.existsSync(skillJsonPath)) {
|
|
120
|
+
throw new Error(`No se encontró skill.json en ${resolvedFolder}`);
|
|
121
|
+
}
|
|
122
|
+
const metadata = JSON.parse(fs.readFileSync(skillJsonPath, 'utf-8'));
|
|
123
|
+
const namespace = opts.namespace || metadata.namespace;
|
|
124
|
+
const name = opts.name || metadata.name;
|
|
125
|
+
const version = opts.version || metadata.version;
|
|
126
|
+
const description = opts.description || metadata.description;
|
|
127
|
+
const tier = opts.tier || metadata.tier || 'free';
|
|
128
|
+
const keywords = metadata.keywords || [];
|
|
129
|
+
const type = (metadata.type || 'DOMAIN').toUpperCase();
|
|
130
|
+
if (!namespace || !name || !description || !version) {
|
|
131
|
+
throw new Error('skill.json requiere namespace, name, description y version');
|
|
132
|
+
}
|
|
133
|
+
if (type !== 'DOMAIN') {
|
|
134
|
+
throw new Error(`Este comando solo publica DOMAIN. Recibido: ${type}`);
|
|
135
|
+
}
|
|
136
|
+
const files = readFilesRecursive(resolvedFolder, resolvedFolder);
|
|
137
|
+
const spinner = (0, ora_1.default)(`Publicando domain skill ${chalk_1.default.cyan(namespace)}/${chalk_1.default.bold(name)}@${version}...`).start();
|
|
138
|
+
try {
|
|
139
|
+
await (0, client_js_1.publishSkill)({
|
|
140
|
+
namespace,
|
|
141
|
+
name,
|
|
142
|
+
description,
|
|
143
|
+
keywords,
|
|
144
|
+
files,
|
|
145
|
+
version,
|
|
146
|
+
type,
|
|
147
|
+
tier,
|
|
148
|
+
});
|
|
149
|
+
spinner.succeed(chalk_1.default.green(`Domain skill publicada: ${namespace}/${name}@${version}`));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (axios_1.default.isAxiosError(error) && error.response?.status === 409) {
|
|
153
|
+
await (0, client_js_1.publishVersion)(namespace, name, { version, files, changelog: 'Imported from local folder via CLI import domain' });
|
|
154
|
+
spinner.succeed(chalk_1.default.green(`Versión publicada sobre skill existente: ${namespace}/${name}@${version}`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
spinner.fail('Error publicando domain skill');
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
(0, errors_js_1.handleApiError)(error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
exports.importCommand = new commander_1.Command('import')
|
|
167
|
+
.description('Comandos de importación')
|
|
168
|
+
.addCommand(mcpImportCommand)
|
|
169
|
+
.addCommand(domainImportCommand);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.installCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const os = __importStar(require("node:os"));
|
|
46
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
47
|
+
const client_js_1 = require("../api/client.js");
|
|
48
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
49
|
+
function parseSkillArg(arg) {
|
|
50
|
+
const atIndex = arg.lastIndexOf('@');
|
|
51
|
+
let fullName;
|
|
52
|
+
let version;
|
|
53
|
+
if (atIndex > 0) {
|
|
54
|
+
fullName = arg.substring(0, atIndex);
|
|
55
|
+
version = arg.substring(atIndex + 1);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
fullName = arg;
|
|
59
|
+
}
|
|
60
|
+
const slashIndex = fullName.lastIndexOf('/');
|
|
61
|
+
if (slashIndex === -1) {
|
|
62
|
+
throw new Error(`Formato inválido. Usa: namespace/name[@version]`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
namespace: fullName.substring(0, slashIndex),
|
|
66
|
+
name: fullName.substring(slashIndex + 1),
|
|
67
|
+
version,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function detectMcpClients() {
|
|
71
|
+
const clients = [];
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
if (fs.existsSync(path.join(home, '.claude')))
|
|
74
|
+
clients.push('claude');
|
|
75
|
+
if (fs.existsSync(path.join(home, '.codex')))
|
|
76
|
+
clients.push('codex');
|
|
77
|
+
if (fs.existsSync(path.join(home, '.cursor')))
|
|
78
|
+
clients.push('cursor');
|
|
79
|
+
return clients;
|
|
80
|
+
}
|
|
81
|
+
const MCP_CONFIG_PATHS = {
|
|
82
|
+
claude: '.claude/mcp.json',
|
|
83
|
+
codex: '.codex/mcp_config.json',
|
|
84
|
+
cursor: '.cursor/mcp.json',
|
|
85
|
+
};
|
|
86
|
+
function writeMcpEntry(client, skillName, mcpConfig) {
|
|
87
|
+
const home = os.homedir();
|
|
88
|
+
const configRelPath = MCP_CONFIG_PATHS[client];
|
|
89
|
+
if (!configRelPath)
|
|
90
|
+
return;
|
|
91
|
+
const configPath = path.join(home, configRelPath);
|
|
92
|
+
let existing = {};
|
|
93
|
+
if (fs.existsSync(configPath)) {
|
|
94
|
+
try {
|
|
95
|
+
existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
existing = {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
const serversKey = 'mcpServers';
|
|
105
|
+
if (!existing[serversKey] || typeof existing[serversKey] !== 'object') {
|
|
106
|
+
existing[serversKey] = {};
|
|
107
|
+
}
|
|
108
|
+
const entry = {};
|
|
109
|
+
if (mcpConfig.url) {
|
|
110
|
+
entry.url = mcpConfig.url;
|
|
111
|
+
}
|
|
112
|
+
if (mcpConfig.command) {
|
|
113
|
+
entry.command = mcpConfig.command;
|
|
114
|
+
}
|
|
115
|
+
if (mcpConfig.args) {
|
|
116
|
+
entry.args = mcpConfig.args;
|
|
117
|
+
}
|
|
118
|
+
if (mcpConfig.env) {
|
|
119
|
+
entry.env = mcpConfig.env;
|
|
120
|
+
}
|
|
121
|
+
existing[serversKey][skillName] = entry;
|
|
122
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
async function installToolOrDomain(namespace, name, targetVersion, installConfig) {
|
|
125
|
+
// Check subscription requirement for DOMAIN
|
|
126
|
+
if (installConfig.type === 'DOMAIN' && installConfig.requiresSubscription) {
|
|
127
|
+
console.log(chalk_1.default.yellow('\n⚠️ Este skill requiere suscripción Pro. Contacta con registry para activarla.'));
|
|
128
|
+
}
|
|
129
|
+
const installDir = path.join(os.homedir(), '.openclaw', 'skills', namespace, name);
|
|
130
|
+
if (fs.existsSync(installDir)) {
|
|
131
|
+
const overwrite = await (0, prompts_1.confirm)({
|
|
132
|
+
message: `${namespace}/${name} ya está instalado. ¿Sobreescribir?`,
|
|
133
|
+
default: false,
|
|
134
|
+
});
|
|
135
|
+
if (!overwrite) {
|
|
136
|
+
console.log(chalk_1.default.yellow('Instalación cancelada'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
const spinner = (0, ora_1.default)(`Instalando ${chalk_1.default.cyan(namespace)}/${chalk_1.default.bold(name)}${chalk_1.default.gray('@' + targetVersion)}...`).start();
|
|
142
|
+
// Try direct download from filesUrl first, fallback to API
|
|
143
|
+
const installConfigData = await (0, client_js_1.getInstallConfig)(namespace, name);
|
|
144
|
+
let files;
|
|
145
|
+
if ('filesUrl' in installConfigData && installConfigData.filesUrl) {
|
|
146
|
+
// Download from Supabase Storage
|
|
147
|
+
const response = await fetch(installConfigData.filesUrl);
|
|
148
|
+
if (response.ok) {
|
|
149
|
+
files = await response.json();
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Fallback to version API
|
|
153
|
+
const versionData = await (0, client_js_1.getVersion)(namespace, name, targetVersion);
|
|
154
|
+
files = versionData.files || {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const versionData = await (0, client_js_1.getVersion)(namespace, name, targetVersion);
|
|
159
|
+
files = versionData.files || {};
|
|
160
|
+
}
|
|
161
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
162
|
+
const fullPath = path.join(installDir, filePath);
|
|
163
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await (0, client_js_1.registerInstall)(namespace, name, targetVersion);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Install tracking is best-effort
|
|
171
|
+
}
|
|
172
|
+
spinner.succeed(chalk_1.default.green(`Instalado en ~/.openclaw/skills/${namespace}/${name}/`));
|
|
173
|
+
}
|
|
174
|
+
async function installMcpServer(name, mcpConfig, targets, targetFlag) {
|
|
175
|
+
let selectedClients;
|
|
176
|
+
if (targetFlag === 'all') {
|
|
177
|
+
selectedClients = detectMcpClients();
|
|
178
|
+
if (selectedClients.length === 0) {
|
|
179
|
+
console.log(chalk_1.default.red('No se detectaron clientes MCP instalados (claude, codex, cursor)'));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (targetFlag) {
|
|
184
|
+
selectedClients = [targetFlag];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Auto-detect and ask
|
|
188
|
+
const available = detectMcpClients();
|
|
189
|
+
if (available.length === 0) {
|
|
190
|
+
console.log(chalk_1.default.red('No se detectaron clientes MCP instalados (claude, codex, cursor)'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (available.length === 1) {
|
|
194
|
+
selectedClients = available;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const chosen = await (0, prompts_1.select)({
|
|
198
|
+
message: '¿En qué cliente MCP quieres instalar?',
|
|
199
|
+
choices: [
|
|
200
|
+
...available.map((c) => ({ name: c, value: c })),
|
|
201
|
+
{ name: 'Todos', value: 'all' },
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
selectedClients = chosen === 'all' ? available : [chosen];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const spinner = (0, ora_1.default)(`Configurando MCP server ${chalk_1.default.bold(name)}...`).start();
|
|
208
|
+
for (const client of selectedClients) {
|
|
209
|
+
writeMcpEntry(client, name, mcpConfig);
|
|
210
|
+
}
|
|
211
|
+
spinner.succeed(chalk_1.default.green(`MCP server ${chalk_1.default.bold(name)} configurado en: ${selectedClients.join(', ')}`));
|
|
212
|
+
}
|
|
213
|
+
exports.installCommand = new commander_1.Command('install')
|
|
214
|
+
.description('Instalar un skill desde el registry')
|
|
215
|
+
.argument('[skill]', 'Skill a instalar (namespace/name[@version])')
|
|
216
|
+
.option('--target <target>', 'Cliente MCP objetivo: claude, codex, cursor, all')
|
|
217
|
+
.option('--license-token <token>', 'Token de licencia del install pack')
|
|
218
|
+
.action(async (skillArg, opts) => {
|
|
219
|
+
try {
|
|
220
|
+
let namespace;
|
|
221
|
+
let name;
|
|
222
|
+
let version;
|
|
223
|
+
if (opts.licenseToken) {
|
|
224
|
+
const consumed = await (0, client_js_1.consumeInstallPack)(opts.licenseToken);
|
|
225
|
+
if (!consumed.ok || !consumed.install) {
|
|
226
|
+
throw new Error(consumed.error || 'No se pudo validar el license token');
|
|
227
|
+
}
|
|
228
|
+
namespace = consumed.install.namespace;
|
|
229
|
+
name = consumed.install.name;
|
|
230
|
+
version = consumed.install.version || undefined;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
if (!skillArg) {
|
|
234
|
+
throw new Error('Debes indicar un skill o usar --license-token');
|
|
235
|
+
}
|
|
236
|
+
const parsed = parseSkillArg(skillArg);
|
|
237
|
+
namespace = parsed.namespace;
|
|
238
|
+
name = parsed.name;
|
|
239
|
+
version = parsed.version;
|
|
240
|
+
}
|
|
241
|
+
// Resolve version
|
|
242
|
+
let targetVersion = version;
|
|
243
|
+
if (!targetVersion) {
|
|
244
|
+
const skill = await (0, client_js_1.getSkill)(namespace, name);
|
|
245
|
+
const latest = skill.versions?.[0];
|
|
246
|
+
if (!latest) {
|
|
247
|
+
console.log(chalk_1.default.red('Este skill no tiene versiones publicadas'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
targetVersion = latest.version;
|
|
251
|
+
}
|
|
252
|
+
// Get install config to determine type
|
|
253
|
+
const installConfig = await (0, client_js_1.getInstallConfig)(namespace, name);
|
|
254
|
+
if (installConfig.type === 'MCP_SERVER') {
|
|
255
|
+
await installMcpServer(name, installConfig.mcpConfig, installConfig.targets, opts.target);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// TOOL or DOMAIN — file-based installation
|
|
259
|
+
await installToolOrDomain(namespace, name, targetVersion, installConfig);
|
|
260
|
+
}
|
|
261
|
+
// Register install (best-effort)
|
|
262
|
+
try {
|
|
263
|
+
await (0, client_js_1.registerInstall)(namespace, name, targetVersion);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// best-effort
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
(0, errors_js_1.handleApiError)(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loginCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const config_js_1 = require("../config.js");
|
|
11
|
+
const client_js_1 = require("../api/client.js");
|
|
12
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
13
|
+
exports.loginCommand = new commander_1.Command('login')
|
|
14
|
+
.description('Guardar token de autenticación')
|
|
15
|
+
.argument('<token>', 'Token de acceso')
|
|
16
|
+
.action(async (token) => {
|
|
17
|
+
const spinner = (0, ora_1.default)('Verificando token...').start();
|
|
18
|
+
try {
|
|
19
|
+
await (0, client_js_1.checkHealth)();
|
|
20
|
+
(0, config_js_1.setToken)(token);
|
|
21
|
+
spinner.succeed(chalk_1.default.green('Token guardado correctamente'));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
spinner.fail('Error al verificar conexión con la API');
|
|
25
|
+
(0, errors_js_1.handleApiError)(error);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.publishCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
46
|
+
const config_js_1 = require("../config.js");
|
|
47
|
+
const client_js_1 = require("../api/client.js");
|
|
48
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
49
|
+
const axios_1 = __importDefault(require("axios"));
|
|
50
|
+
const EXCLUDED_DIRS = ['node_modules', '.git', '.env', 'dist', '__pycache__'];
|
|
51
|
+
function readFilesRecursive(dir, baseDir) {
|
|
52
|
+
const files = {};
|
|
53
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (EXCLUDED_DIRS.includes(entry.name))
|
|
56
|
+
continue;
|
|
57
|
+
if (entry.name.startsWith('.env'))
|
|
58
|
+
continue;
|
|
59
|
+
const fullPath = path.join(dir, entry.name);
|
|
60
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
Object.assign(files, readFilesRecursive(fullPath, baseDir));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
try {
|
|
66
|
+
files[relativePath] = fs.readFileSync(fullPath, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Skip binary/unreadable files
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
exports.publishCommand = new commander_1.Command('publish')
|
|
76
|
+
.description('Publicar un skill en el registry')
|
|
77
|
+
.argument('[folder]', 'Carpeta del skill', './')
|
|
78
|
+
.action(async (folder) => {
|
|
79
|
+
try {
|
|
80
|
+
const token = (0, config_js_1.getToken)();
|
|
81
|
+
if (!token) {
|
|
82
|
+
console.log(chalk_1.default.red("Ejecuta 'openclaw-skills login <token>' primero"));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const resolvedFolder = path.resolve(folder);
|
|
86
|
+
const skillMdPath = path.join(resolvedFolder, 'SKILL.md');
|
|
87
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
88
|
+
console.log(chalk_1.default.red('No se encontró SKILL.md en ' + resolvedFolder));
|
|
89
|
+
console.log(chalk_1.default.yellow('SKILL.md es obligatorio para publicar un skill'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Try to read metadata from skill.json or package.json
|
|
93
|
+
let metadata = {};
|
|
94
|
+
const skillJsonPath = path.join(resolvedFolder, 'skill.json');
|
|
95
|
+
const packageJsonPath = path.join(resolvedFolder, 'package.json');
|
|
96
|
+
if (fs.existsSync(skillJsonPath)) {
|
|
97
|
+
metadata = JSON.parse(fs.readFileSync(skillJsonPath, 'utf-8'));
|
|
98
|
+
}
|
|
99
|
+
else if (fs.existsSync(packageJsonPath)) {
|
|
100
|
+
metadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
const folderName = path.basename(resolvedFolder);
|
|
103
|
+
// Skill type selection
|
|
104
|
+
const skillType = await (0, prompts_1.select)({
|
|
105
|
+
message: 'Tipo de skill:',
|
|
106
|
+
choices: [
|
|
107
|
+
{ name: '🔧 Tool integration', value: 'TOOL' },
|
|
108
|
+
{ name: '⚡ MCP Server', value: 'MCP_SERVER' },
|
|
109
|
+
{ name: '🧠 Domain skill', value: 'DOMAIN' },
|
|
110
|
+
],
|
|
111
|
+
default: metadata.type || 'TOOL',
|
|
112
|
+
});
|
|
113
|
+
// MCP config if MCP_SERVER
|
|
114
|
+
let mcpConfig;
|
|
115
|
+
if (skillType === 'MCP_SERVER') {
|
|
116
|
+
console.log(chalk_1.default.cyan('\nConfiguración MCP Server:'));
|
|
117
|
+
const transport = await (0, prompts_1.select)({
|
|
118
|
+
message: 'Tipo de transporte:',
|
|
119
|
+
choices: [
|
|
120
|
+
{ name: 'HTTP (URL remota)', value: 'http' },
|
|
121
|
+
{ name: 'stdio (comando local)', value: 'stdio' },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
mcpConfig = { transport };
|
|
125
|
+
if (transport === 'http') {
|
|
126
|
+
const url = await (0, prompts_1.input)({
|
|
127
|
+
message: 'URL del MCP server:',
|
|
128
|
+
validate: (v) => (v.startsWith('http') ? true : 'Debe ser una URL válida'),
|
|
129
|
+
});
|
|
130
|
+
mcpConfig.url = url;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const command = await (0, prompts_1.input)({
|
|
134
|
+
message: 'Comando a ejecutar:',
|
|
135
|
+
validate: (v) => (v.length > 0 ? true : 'El comando es obligatorio'),
|
|
136
|
+
});
|
|
137
|
+
mcpConfig.command = command;
|
|
138
|
+
const argsInput = await (0, prompts_1.input)({
|
|
139
|
+
message: 'Argumentos (separados por espacio, vacío para ninguno):',
|
|
140
|
+
});
|
|
141
|
+
if (argsInput.trim()) {
|
|
142
|
+
mcpConfig.args = argsInput.trim().split(/\s+/);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Tier selection
|
|
147
|
+
const tier = await (0, prompts_1.select)({
|
|
148
|
+
message: 'Tier:',
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: 'Free', value: 'free' },
|
|
151
|
+
{ name: 'Pro', value: 'pro' },
|
|
152
|
+
{ name: 'Enterprise', value: 'enterprise' },
|
|
153
|
+
],
|
|
154
|
+
default: metadata.tier || 'free',
|
|
155
|
+
});
|
|
156
|
+
const namespace = await (0, prompts_1.input)({
|
|
157
|
+
message: 'Namespace:',
|
|
158
|
+
default: metadata.namespace || `io.github.${process.env.USER || 'user'}`,
|
|
159
|
+
});
|
|
160
|
+
const name = await (0, prompts_1.input)({
|
|
161
|
+
message: 'Nombre del skill:',
|
|
162
|
+
default: metadata.name || folderName,
|
|
163
|
+
});
|
|
164
|
+
const description = await (0, prompts_1.input)({
|
|
165
|
+
message: 'Descripción:',
|
|
166
|
+
default: metadata.description || '',
|
|
167
|
+
});
|
|
168
|
+
const version = await (0, prompts_1.input)({
|
|
169
|
+
message: 'Versión:',
|
|
170
|
+
default: metadata.version || '1.0.0',
|
|
171
|
+
});
|
|
172
|
+
const keywordsInput = await (0, prompts_1.input)({
|
|
173
|
+
message: 'Keywords (separadas por coma):',
|
|
174
|
+
default: Array.isArray(metadata.keywords)
|
|
175
|
+
? metadata.keywords.join(', ')
|
|
176
|
+
: '',
|
|
177
|
+
});
|
|
178
|
+
const keywords = keywordsInput
|
|
179
|
+
.split(',')
|
|
180
|
+
.map((k) => k.trim())
|
|
181
|
+
.filter(Boolean);
|
|
182
|
+
const spinner = (0, ora_1.default)(`Publicando ${chalk_1.default.bold(name)}${chalk_1.default.gray('@' + version)}...`).start();
|
|
183
|
+
const files = readFilesRecursive(resolvedFolder, resolvedFolder);
|
|
184
|
+
try {
|
|
185
|
+
await (0, client_js_1.publishSkill)({
|
|
186
|
+
namespace,
|
|
187
|
+
name,
|
|
188
|
+
description,
|
|
189
|
+
keywords,
|
|
190
|
+
files,
|
|
191
|
+
version,
|
|
192
|
+
type: skillType,
|
|
193
|
+
mcpConfig,
|
|
194
|
+
tier,
|
|
195
|
+
});
|
|
196
|
+
spinner.succeed(chalk_1.default.green(`Skill publicado: ${namespace}/${name}`));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (axios_1.default.isAxiosError(error) && error.response?.status === 409) {
|
|
200
|
+
spinner.warn(chalk_1.default.yellow(`${namespace}/${name} ya existe en el registry`));
|
|
201
|
+
const publishNew = await (0, prompts_1.confirm)({
|
|
202
|
+
message: `¿Publicar nueva versión ${version}?`,
|
|
203
|
+
default: true,
|
|
204
|
+
});
|
|
205
|
+
if (!publishNew) {
|
|
206
|
+
console.log(chalk_1.default.yellow('Publicación cancelada'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const versionSpinner = (0, ora_1.default)(`Publicando versión ${version}...`).start();
|
|
210
|
+
await (0, client_js_1.publishVersion)(namespace, name, { version, files });
|
|
211
|
+
versionSpinner.succeed(chalk_1.default.green(`Versión ${version} publicada: ${namespace}/${name}`));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
spinner.fail('Error al publicar');
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
(0, errors_js_1.handleApiError)(error);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.searchCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const client_js_1 = require("../api/client.js");
|
|
11
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
12
|
+
const TYPE_EMOJI = {
|
|
13
|
+
TOOL: '🔧',
|
|
14
|
+
MCP_SERVER: '⚡',
|
|
15
|
+
DOMAIN: '🧠',
|
|
16
|
+
};
|
|
17
|
+
exports.searchCommand = new commander_1.Command('search')
|
|
18
|
+
.description('Buscar skills en el registry')
|
|
19
|
+
.argument('[query]', 'Término de búsqueda')
|
|
20
|
+
.option('--keyword <kw>', 'Filtrar por keyword')
|
|
21
|
+
.option('--sort <sort>', 'Ordenar por: installs, recent, name', 'recent')
|
|
22
|
+
.option('--limit <n>', 'Número de resultados', '10')
|
|
23
|
+
.option('--type <type>', 'Filtrar por tipo: TOOL, MCP_SERVER, DOMAIN')
|
|
24
|
+
.option('--certified', 'Mostrar solo skills certificados')
|
|
25
|
+
.action(async (query, opts) => {
|
|
26
|
+
const spinner = (0, ora_1.default)('Buscando skills...').start();
|
|
27
|
+
try {
|
|
28
|
+
const result = await (0, client_js_1.listSkills)({
|
|
29
|
+
q: query,
|
|
30
|
+
keyword: opts.keyword,
|
|
31
|
+
sort: opts.sort,
|
|
32
|
+
limit: parseInt(opts.limit, 10),
|
|
33
|
+
type: opts.type,
|
|
34
|
+
certified: opts.certified || false,
|
|
35
|
+
});
|
|
36
|
+
spinner.stop();
|
|
37
|
+
const skills = result.data;
|
|
38
|
+
if (!skills || skills.length === 0) {
|
|
39
|
+
console.log(chalk_1.default.yellow('No se encontraron skills para esa búsqueda'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`\n📦 ${chalk_1.default.bold(skills.length)} skills encontrados\n`);
|
|
43
|
+
for (const skill of skills) {
|
|
44
|
+
const latestVersion = skill.versions?.[0];
|
|
45
|
+
const version = latestVersion?.version || 'N/A';
|
|
46
|
+
const keywords = skill.keywords || [];
|
|
47
|
+
const skillType = skill.type || 'TOOL';
|
|
48
|
+
const emoji = TYPE_EMOJI[skillType] || '📦';
|
|
49
|
+
const tier = skill.tier || 'free';
|
|
50
|
+
let tierBadge = '';
|
|
51
|
+
if (tier === 'pro') {
|
|
52
|
+
tierBadge = chalk_1.default.yellow(' [PRO]');
|
|
53
|
+
}
|
|
54
|
+
else if (tier === 'enterprise') {
|
|
55
|
+
tierBadge = chalk_1.default.yellow(' [ENTERPRISE]');
|
|
56
|
+
}
|
|
57
|
+
const certBadge = skill.certified ? chalk_1.default.green(' ✓ certified') : '';
|
|
58
|
+
console.log(` ${emoji} ${chalk_1.default.cyan(skill.namespace)}/${chalk_1.default.bold(skill.name)}${chalk_1.default.gray('@' + version)}${tierBadge}${certBadge}`);
|
|
59
|
+
console.log(` ${skill.description || 'Sin descripción'}`);
|
|
60
|
+
if (keywords.length > 0) {
|
|
61
|
+
console.log(` Keywords: ${chalk_1.default.yellow(keywords.join(', '))}`);
|
|
62
|
+
}
|
|
63
|
+
console.log(` Installs: ${skill.totalInstalls ?? skill.installCount ?? 0}`);
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
spinner.fail('Error al buscar skills');
|
|
69
|
+
(0, errors_js_1.handleApiError)(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getToken = getToken;
|
|
7
|
+
exports.setToken = setToken;
|
|
8
|
+
exports.getApiUrl = getApiUrl;
|
|
9
|
+
exports.setApiUrl = setApiUrl;
|
|
10
|
+
const conf_1 = __importDefault(require("conf"));
|
|
11
|
+
const config = new conf_1.default({
|
|
12
|
+
projectName: 'openclaw-skills',
|
|
13
|
+
});
|
|
14
|
+
function getToken() {
|
|
15
|
+
return config.get('token');
|
|
16
|
+
}
|
|
17
|
+
function setToken(token) {
|
|
18
|
+
config.set('token', token);
|
|
19
|
+
}
|
|
20
|
+
function getApiUrl() {
|
|
21
|
+
const envUrl = process.env.SKILLS_REGISTRY_URL;
|
|
22
|
+
if (envUrl)
|
|
23
|
+
return envUrl;
|
|
24
|
+
return config.get('apiUrl') || 'http://localhost:3000';
|
|
25
|
+
}
|
|
26
|
+
function setApiUrl(url) {
|
|
27
|
+
config.set('apiUrl', url);
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleApiError = handleApiError;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
function handleApiError(error) {
|
|
10
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
11
|
+
const status = error.response?.status;
|
|
12
|
+
const message = error.response?.data?.error || error.response?.data?.message;
|
|
13
|
+
if (status === 401) {
|
|
14
|
+
console.log(chalk_1.default.red('Token inválido o expirado. Ejecuta login de nuevo.'));
|
|
15
|
+
}
|
|
16
|
+
else if (status === 404) {
|
|
17
|
+
console.log(chalk_1.default.red('Skill no encontrado'));
|
|
18
|
+
}
|
|
19
|
+
else if (status === 409) {
|
|
20
|
+
console.log(chalk_1.default.red(message || 'Conflicto: el recurso ya existe'));
|
|
21
|
+
}
|
|
22
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
23
|
+
console.log(chalk_1.default.red('No se pudo conectar con la API. ¿Está corriendo el servidor?'));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log(chalk_1.default.red(message || `Error del servidor (${status || 'desconocido'})`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (error instanceof Error) {
|
|
30
|
+
if (error.code === 'ECONNREFUSED') {
|
|
31
|
+
console.log(chalk_1.default.red('No se pudo conectar con la API. ¿Está corriendo el servidor?'));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(chalk_1.default.red(error.message));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-skills-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for OpenClaw Skills Registry",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-skills": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/cli.ts",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"lint": "eslint src --ext .ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"skills",
|
|
18
|
+
"cli",
|
|
19
|
+
"registry"
|
|
20
|
+
],
|
|
21
|
+
"author": "OpenClaw Team",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@inquirer/prompts": "^8.2.1",
|
|
25
|
+
"ajv": "^8.12.0",
|
|
26
|
+
"axios": "^1.6.7",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"cli-progress": "^3.12.0",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"conf": "^12.0.0",
|
|
31
|
+
"inquirer": "^9.2.15",
|
|
32
|
+
"marked": "^12.0.0",
|
|
33
|
+
"ora": "^8.0.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/inquirer": "^9.0.7",
|
|
37
|
+
"@types/node": "^20.11.20",
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
|
39
|
+
"@typescript-eslint/parser": "^7.0.2",
|
|
40
|
+
"eslint": "^8.56.0",
|
|
41
|
+
"jest": "^29.7.0",
|
|
42
|
+
"ts-jest": "^29.1.2",
|
|
43
|
+
"tsx": "^4.7.1",
|
|
44
|
+
"typescript": "^5.3.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|