citaspro-cli 1.0.2
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/README.md +68 -0
- package/citasproctl.js +1021 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# citaspro-cli
|
|
2
|
+
|
|
3
|
+
CLI para gestionar leads, reservas y calendario de CitasPro.
|
|
4
|
+
|
|
5
|
+
## Instalacion
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g citaspro-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Inicio rapido
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
citaspro-cli login
|
|
15
|
+
citaspro-cli whoami
|
|
16
|
+
citaspro-cli leads:list
|
|
17
|
+
citaspro-cli bookings:list
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Autenticacion
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
citaspro-cli login
|
|
24
|
+
citaspro-cli whoami
|
|
25
|
+
citaspro-cli logout
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Comandos principales
|
|
29
|
+
|
|
30
|
+
### Leads
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
citaspro-cli leads:list
|
|
34
|
+
citaspro-cli leads:get <lead_id|email|telefono>
|
|
35
|
+
citaspro-cli leads:create '{"nombre":"Juan","email":"juan@demo.com","telefono":"+34612345678"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Reservas
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
citaspro-cli bookings:list
|
|
42
|
+
citaspro-cli tools:availability 2026-02-20 10:00 10:30
|
|
43
|
+
citaspro-cli tools:create-booking '{"fecha":"2026-02-20","hora_inicio":"10:00","hora_fin":"10:30","contacto_nombre":"Juan","contacto_email":"juan@demo.com"}'
|
|
44
|
+
citaspro-cli bookings:reschedule <booking_id> 2026-02-21 11:00 11:30
|
|
45
|
+
citaspro-cli tools:cancel-booking <booking_id>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Calendario y llamadas
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
citaspro-cli tools:caldav
|
|
52
|
+
citaspro-cli tools:call +34612345678 "Recordatorio de cita"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Modo agente / automatizacion
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
citaspro-cli ai:manifest
|
|
59
|
+
citaspro-cli whoami --json
|
|
60
|
+
citaspro-cli leads:list 20 --json
|
|
61
|
+
citaspro-cli bookings:list 20 --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Ayuda
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
citaspro-cli --help
|
|
68
|
+
```
|
package/citasproctl.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
require('dotenv').config();
|
|
4
|
+
} catch (_error) {
|
|
5
|
+
// dotenv es opcional para el CLI
|
|
6
|
+
}
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { exec, execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
function isEnabled(value) {
|
|
14
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeBaseUrl(url) {
|
|
18
|
+
return String(url || '').replace(/\/+$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveApiBase() {
|
|
22
|
+
if (process.env.CITAS_API_URL) {
|
|
23
|
+
return normalizeBaseUrl(process.env.CITAS_API_URL);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useLocalhost = isEnabled(process.env.CITAS_USE_LOCALHOST) || isEnabled(process.env.CITAS_CLI_USE_LOCALHOST);
|
|
27
|
+
if (useLocalhost) {
|
|
28
|
+
const localPort = process.env.CITAS_LOCAL_PORT || process.env.PORT || '3000';
|
|
29
|
+
return `http://localhost:${localPort}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return 'https://citaspro.cloud';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const API_BASE = resolveApiBase();
|
|
36
|
+
const AUTH_FILE = process.env.CITAS_CLI_AUTH_FILE || path.join(os.homedir(), '.citas-cli-auth.json');
|
|
37
|
+
|
|
38
|
+
const USE_COLOR = Boolean(process.stdout.isTTY) && process.env.NO_COLOR !== '1';
|
|
39
|
+
const ANSI = {
|
|
40
|
+
reset: '\x1b[0m',
|
|
41
|
+
bold: '\x1b[1m',
|
|
42
|
+
dim: '\x1b[2m',
|
|
43
|
+
red: '\x1b[31m',
|
|
44
|
+
green: '\x1b[32m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
cyan: '\x1b[36m',
|
|
47
|
+
gray: '\x1b[90m',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function colorize(text, color) {
|
|
51
|
+
if (!USE_COLOR) {
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
return `${ANSI[color] || ''}${text}${ANSI.reset}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function bold(text) {
|
|
58
|
+
return colorize(text, 'bold');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function usage() {
|
|
62
|
+
console.log(`
|
|
63
|
+
CitasPro CLI
|
|
64
|
+
============
|
|
65
|
+
|
|
66
|
+
${bold('USO:')}
|
|
67
|
+
node tools/citas-cli.js <comando> [argumentos] [--json]
|
|
68
|
+
node tools/citas-cli.js --openclaw [nombre_agente]
|
|
69
|
+
node tools/citas-cli.js --openclaw [nombre_agente] --whatsapp
|
|
70
|
+
|
|
71
|
+
${bold('ENTORNO API:')}
|
|
72
|
+
Default: https://citaspro.cloud
|
|
73
|
+
Override directo: CITAS_API_URL=https://mi-api
|
|
74
|
+
Modo local: CITAS_USE_LOCALHOST=1
|
|
75
|
+
Puerto local opcional: CITAS_LOCAL_PORT=3000
|
|
76
|
+
(alias modo local): CITAS_CLI_USE_LOCALHOST=1
|
|
77
|
+
|
|
78
|
+
${bold('INICIO RAPIDO (HUMANO):')}
|
|
79
|
+
node tools/citas-cli.js login
|
|
80
|
+
node tools/citas-cli.js whoami
|
|
81
|
+
node tools/citas-cli.js leads:list
|
|
82
|
+
node tools/citas-cli.js bookings:list
|
|
83
|
+
|
|
84
|
+
${bold('MODO IA / AGENTE:')}
|
|
85
|
+
${colorize('--json', 'cyan')} Fuerza salida JSON estable
|
|
86
|
+
${colorize('--openclaw', 'cyan')} [nombre_agente] Crea subagente OpenClaw en ~/clawd
|
|
87
|
+
${colorize('--whatsapp', 'cyan')} Configura login del canal WhatsApp
|
|
88
|
+
${colorize('ai:manifest', 'cyan')} Describe comandos, argumentos y salida
|
|
89
|
+
node tools/citas-cli.js whoami --json
|
|
90
|
+
|
|
91
|
+
${bold('AUTENTICACION:')}
|
|
92
|
+
${colorize('login', 'cyan')} OAuth Device Flow (abre navegador y espera)
|
|
93
|
+
${colorize('logout', 'cyan')} Elimina sesión local
|
|
94
|
+
${colorize('whoami', 'cyan')} Muestra usuario, tenant y tipo auth
|
|
95
|
+
|
|
96
|
+
${bold('LEADS:')}
|
|
97
|
+
${colorize('leads:list', 'cyan')} [limit] Lista leads (default: 20)
|
|
98
|
+
${colorize('leads:get', 'cyan')} <id|email|phone> Obtiene un lead
|
|
99
|
+
${colorize('leads:create', 'cyan')} '<json>' Crea lead (nombre + email obligatorios)
|
|
100
|
+
|
|
101
|
+
${bold('RESERVAS:')}
|
|
102
|
+
${colorize('bookings:list', 'cyan')} [limit] Lista reservas
|
|
103
|
+
${colorize('tools:availability', 'cyan')} <fecha> <hora_inicio> <hora_fin>
|
|
104
|
+
Valida disponibilidad
|
|
105
|
+
${colorize('tools:create-booking', 'cyan')} '<json>' Crea reserva
|
|
106
|
+
${colorize('bookings:reschedule', 'cyan')} <id> <fecha> <hora_inicio> <hora_fin>
|
|
107
|
+
Reprograma reserva
|
|
108
|
+
${colorize('tools:cancel-booking', 'cyan')} <id> Cancela reserva
|
|
109
|
+
|
|
110
|
+
${bold('CALENDARIO / LLAMADAS:')}
|
|
111
|
+
${colorize('tools:caldav', 'cyan')} Devuelve enlace CalDAV
|
|
112
|
+
${colorize('tools:call', 'cyan')} <to> [message] Llamada (real o simulada)
|
|
113
|
+
|
|
114
|
+
${bold('DEVICE FLOW (MANUAL):')}
|
|
115
|
+
${colorize('device:start', 'cyan')} Genera device_code + user_code
|
|
116
|
+
${colorize('device:poll', 'cyan')} <device_code> Intercambia device_code por token
|
|
117
|
+
|
|
118
|
+
${bold('ALIASES (cortos):')}
|
|
119
|
+
leads ls [limit]
|
|
120
|
+
leads get <id|email|phone>
|
|
121
|
+
leads add '<json>'
|
|
122
|
+
bookings ls [limit]
|
|
123
|
+
bookings mv <id> <fecha> <hora_inicio> <hora_fin>
|
|
124
|
+
bookings rm <id>
|
|
125
|
+
calendar avail <fecha> <hora_inicio> <hora_fin>
|
|
126
|
+
calendar caldav
|
|
127
|
+
call <to> [message]
|
|
128
|
+
|
|
129
|
+
${bold('EJEMPLOS:')}
|
|
130
|
+
node tools/citas-cli.js --openclaw ventas-bot
|
|
131
|
+
node tools/citas-cli.js --openclaw ventas-bot --whatsapp
|
|
132
|
+
node tools/citas-cli.js leads:create '{"nombre":"Juan","email":"juan@demo.com","telefono":"+34612345678"}'
|
|
133
|
+
node tools/citas-cli.js tools:create-booking '{"fecha":"2026-02-20","hora_inicio":"10:00","hora_fin":"10:30","contacto_nombre":"Juan","contacto_email":"juan@demo.com"}'
|
|
134
|
+
node tools/citas-cli.js bookings:list 20 --json
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const AI_MANIFEST = {
|
|
139
|
+
name: 'citaspro-cli',
|
|
140
|
+
version: '1.0.0',
|
|
141
|
+
base_url: API_BASE,
|
|
142
|
+
auth: {
|
|
143
|
+
mode: 'device_flow_local_session',
|
|
144
|
+
login_command: 'node tools/citas-cli.js login',
|
|
145
|
+
},
|
|
146
|
+
global_flags: ['--json', '--openclaw', '--whatsapp'],
|
|
147
|
+
commands: [
|
|
148
|
+
{ id: 'whoami', args: [], returns: ['user', 'tenant_id', 'auth_type'] },
|
|
149
|
+
{ id: 'leads:list', args: ['limit?'], returns: ['count', 'leads[]'] },
|
|
150
|
+
{ id: 'leads:get', args: ['id|email|phone'], returns: ['lead'] },
|
|
151
|
+
{ id: 'leads:create', args: ['json_payload'], returns: ['lead'] },
|
|
152
|
+
{ id: 'bookings:list', args: ['limit?'], returns: ['count', 'bookings[]'] },
|
|
153
|
+
{ id: 'tools:availability', args: ['fecha', 'hora_inicio', 'hora_fin'], returns: ['available', 'reasons', 'conflicts[]'] },
|
|
154
|
+
{ id: 'tools:create-booking', args: ['json_payload'], returns: ['booking'] },
|
|
155
|
+
{ id: 'bookings:reschedule', args: ['booking_id', 'fecha', 'hora_inicio', 'hora_fin'], returns: ['booking'] },
|
|
156
|
+
{ id: 'tools:cancel-booking', args: ['booking_id'], returns: ['booking'] },
|
|
157
|
+
{ id: 'tools:caldav', args: [], returns: ['caldav_url', 'radicale_username', 'calendar_id'] },
|
|
158
|
+
{ id: 'tools:call', args: ['to', 'message?'], returns: ['status', 'provider', 'to'] },
|
|
159
|
+
{ id: 'openclaw:init', args: ['agent_name?'], returns: ['output_dir', 'files[]', 'prompt_preview'] },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function looksLikeToken(value) {
|
|
164
|
+
if (typeof value !== 'string') {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return value.startsWith('cli_') || value.startsWith('rcli_') || value.split('.').length === 3;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function looksLikePayload(value) {
|
|
171
|
+
if (typeof value !== 'string') {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const trimmed = value.trim();
|
|
175
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readSession() {
|
|
179
|
+
try {
|
|
180
|
+
if (!fs.existsSync(AUTH_FILE)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const raw = fs.readFileSync(AUTH_FILE, 'utf8');
|
|
185
|
+
const parsed = JSON.parse(raw);
|
|
186
|
+
|
|
187
|
+
if (!parsed?.access_token) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return parsed;
|
|
192
|
+
} catch (_error) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeSession(session) {
|
|
198
|
+
const dir = path.dirname(AUTH_FILE);
|
|
199
|
+
if (!fs.existsSync(dir)) {
|
|
200
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(session, null, 2), 'utf8');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function clearSession() {
|
|
206
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
207
|
+
fs.unlinkSync(AUTH_FILE);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function post(pathname, data = {}, token = null) {
|
|
212
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
213
|
+
if (token) {
|
|
214
|
+
headers.Authorization = `Bearer ${token}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const response = await axios.post(`${API_BASE}${pathname}`, data, {
|
|
218
|
+
headers,
|
|
219
|
+
timeout: 15000,
|
|
220
|
+
// Evita problemas de proxy/entorno que provocan ECONNRESET "socket hang up"
|
|
221
|
+
proxy: false,
|
|
222
|
+
});
|
|
223
|
+
return response.data;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function get(pathname, token = null) {
|
|
227
|
+
const headers = {};
|
|
228
|
+
if (token) {
|
|
229
|
+
headers.Authorization = `Bearer ${token}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const response = await axios.get(`${API_BASE}${pathname}`, {
|
|
233
|
+
headers,
|
|
234
|
+
timeout: 15000,
|
|
235
|
+
proxy: false,
|
|
236
|
+
});
|
|
237
|
+
return response.data;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sanitizeAgentName(name) {
|
|
241
|
+
const raw = String(name || '').trim() || 'nombreagente';
|
|
242
|
+
const sanitized = raw
|
|
243
|
+
.normalize('NFD')
|
|
244
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
245
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-')
|
|
246
|
+
.replace(/-+/g, '-')
|
|
247
|
+
.replace(/^-|-$/g, '');
|
|
248
|
+
return sanitized || 'nombreagente';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function fetchPromptFromAi(token) {
|
|
252
|
+
const fallback = {
|
|
253
|
+
prompt: 'Eres un asistente profesional para gestión de citas.',
|
|
254
|
+
tone: 'neutro',
|
|
255
|
+
templateName: '',
|
|
256
|
+
companyData: {},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const data = await get('/api/prompts-and-templates', token);
|
|
261
|
+
const templates = Array.isArray(data?.templates) ? data.templates : [];
|
|
262
|
+
const defaultName = String(data?.defaultTemplateName || '').trim();
|
|
263
|
+
const selected = templates.find((t) => t?.isDefault || t?.name === defaultName) || templates[0];
|
|
264
|
+
|
|
265
|
+
if (!selected) {
|
|
266
|
+
return fallback;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
prompt: String(selected?.prompt || fallback.prompt).trim() || fallback.prompt,
|
|
271
|
+
tone: String(selected?.tone || fallback.tone).trim() || fallback.tone,
|
|
272
|
+
templateName: String(selected?.name || '').trim(),
|
|
273
|
+
companyData: selected?.companyData || selected?.company_data || {},
|
|
274
|
+
};
|
|
275
|
+
} catch (_error) {
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildOpenClawFiles({ agentName, promptConfig, whatsapp }) {
|
|
281
|
+
const companyName = String(promptConfig?.companyData?.name || 'CitasPro');
|
|
282
|
+
const promptText = String(promptConfig?.prompt || 'Eres un asistente profesional para gestión de citas.');
|
|
283
|
+
const tone = String(promptConfig?.tone || 'neutro');
|
|
284
|
+
|
|
285
|
+
const soul = `# SOUL
|
|
286
|
+
|
|
287
|
+
## Identidad
|
|
288
|
+
Eres un subagente de OpenClaw orientado a operación comercial y agenda en CitasPro.
|
|
289
|
+
|
|
290
|
+
## Misión
|
|
291
|
+
- Capturar y actualizar leads.
|
|
292
|
+
- Crear, mover y cancelar reservas.
|
|
293
|
+
- Mantener sincronización entre CRM y calendario.
|
|
294
|
+
|
|
295
|
+
## Aislamiento
|
|
296
|
+
- Este agente usa workspace dedicado.
|
|
297
|
+
- No debe leer ni operar datos de otros agentes/tenants.
|
|
298
|
+
- Solo ejecutar acciones sobre su contexto asignado.
|
|
299
|
+
|
|
300
|
+
## Prompt base extraído de /ai
|
|
301
|
+
\`\`\`text
|
|
302
|
+
${promptText}
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
## Contexto de negocio
|
|
306
|
+
- Empresa: ${companyName}
|
|
307
|
+
- Tono: ${tone}
|
|
308
|
+
|
|
309
|
+
## LLM recomendado
|
|
310
|
+
- Provider: OpenRouter
|
|
311
|
+
- Modelo sugerido: kimi-k2
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
const skill = `---
|
|
315
|
+
name: ${agentName}
|
|
316
|
+
description: Subagente para operar CitasPro (leads, reservas y calendario) desde OpenClaw.
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
# SKILL
|
|
320
|
+
|
|
321
|
+
## Flujo recomendado
|
|
322
|
+
1. Identidad del operador
|
|
323
|
+
- \`citaspro-cli whoami --json\`
|
|
324
|
+
|
|
325
|
+
2. Leads
|
|
326
|
+
- \`citaspro-cli leads:list 20 --json\`
|
|
327
|
+
- \`citaspro-cli get <id|email|telefono> --json\`
|
|
328
|
+
- \`citaspro-cli leads:create '<json>' --json\`
|
|
329
|
+
|
|
330
|
+
3. Agenda
|
|
331
|
+
- \`citaspro-cli tools:availability <fecha> <inicio> <fin> --json\`
|
|
332
|
+
- \`citaspro-cli tools:create-booking '<json>' --json\`
|
|
333
|
+
- \`citaspro-cli bookings:reschedule <id> <fecha> <inicio> <fin> --json\`
|
|
334
|
+
- \`citaspro-cli tools:cancel-booking <id> --json\`
|
|
335
|
+
|
|
336
|
+
4. Calendario
|
|
337
|
+
- \`citaspro-cli tools:caldav --json\`
|
|
338
|
+
|
|
339
|
+
## Recomendación de modelo
|
|
340
|
+
- Usar OpenRouter con modelo \`kimi-k2\` para este agente.
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
const heardbeat = `# HEARDBEAT
|
|
344
|
+
|
|
345
|
+
## Checks
|
|
346
|
+
1. Auth
|
|
347
|
+
\`\`\`bash
|
|
348
|
+
citaspro-cli whoami --json
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
2. Leads
|
|
352
|
+
\`\`\`bash
|
|
353
|
+
citaspro-cli leads:list 1 --json
|
|
354
|
+
\`\`\`
|
|
355
|
+
|
|
356
|
+
3. Agenda
|
|
357
|
+
\`\`\`bash
|
|
358
|
+
citaspro-cli bookings:list 1 --json
|
|
359
|
+
\`\`\`
|
|
360
|
+
|
|
361
|
+
4. CalDAV
|
|
362
|
+
\`\`\`bash
|
|
363
|
+
citaspro-cli tools:caldav --json
|
|
364
|
+
\`\`\`
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
const channels = `# CHANNELS
|
|
368
|
+
|
|
369
|
+
## WhatsApp (channel scope)
|
|
370
|
+
- allow all: true
|
|
371
|
+
- account: ${whatsapp?.account || 'citaspro'}
|
|
372
|
+
|
|
373
|
+
Ejemplo:
|
|
374
|
+
\`\`\`json
|
|
375
|
+
{
|
|
376
|
+
"channels": {
|
|
377
|
+
"whatsapp": {
|
|
378
|
+
"dmPolicy": "pairing",
|
|
379
|
+
"groupPolicy": "allowlist",
|
|
380
|
+
"allowFrom": [],
|
|
381
|
+
"groupAllowFrom": [],
|
|
382
|
+
"allowAll": true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
\`\`\`
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
return { soul, skill, heardbeat, channels };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function createOpenClawSubagent(agentName, token, options = {}) {
|
|
393
|
+
const safeName = sanitizeAgentName(agentName);
|
|
394
|
+
const rootDir = process.env.OPENCLAW_AGENTS_DIR || path.join(os.homedir(), 'clawd');
|
|
395
|
+
const agentDir = path.join(rootDir, safeName);
|
|
396
|
+
const skillDir = path.join(agentDir, 'skills', 'citaspro');
|
|
397
|
+
const promptConfig = await fetchPromptFromAi(token);
|
|
398
|
+
const whatsapp = {
|
|
399
|
+
enabled: Boolean(options?.whatsapp),
|
|
400
|
+
allow_all: true,
|
|
401
|
+
account: 'citaspro',
|
|
402
|
+
};
|
|
403
|
+
const files = buildOpenClawFiles({ agentName: safeName, promptConfig, whatsapp });
|
|
404
|
+
|
|
405
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
406
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
407
|
+
fs.writeFileSync(path.join(agentDir, 'SOUL.md'), files.soul, 'utf8');
|
|
408
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), files.skill, 'utf8');
|
|
409
|
+
fs.writeFileSync(path.join(agentDir, 'HEARDBEAT.md'), files.heardbeat, 'utf8');
|
|
410
|
+
fs.writeFileSync(path.join(agentDir, 'CHANNELS.md'), files.channels, 'utf8');
|
|
411
|
+
|
|
412
|
+
// Intento de creación real del agente en OpenClaw (si el binario existe).
|
|
413
|
+
const openclaw = {
|
|
414
|
+
attempted: false,
|
|
415
|
+
created: false,
|
|
416
|
+
identity_set: false,
|
|
417
|
+
listed: false,
|
|
418
|
+
command: null,
|
|
419
|
+
details: null,
|
|
420
|
+
steps: [],
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const createMode = String(process.env.OPENCLAW_CREATE_MODE || 'auto').toLowerCase();
|
|
424
|
+
if (createMode !== 'files-only') {
|
|
425
|
+
openclaw.attempted = true;
|
|
426
|
+
const escaped = safeName.replace(/"/g, '\\"');
|
|
427
|
+
const workspaceEscaped = agentDir.replace(/"/g, '\\"');
|
|
428
|
+
const identityName = String(process.env.OPENCLAW_AGENT_NAME || safeName);
|
|
429
|
+
const identityEmoji = String(process.env.OPENCLAW_AGENT_EMOJI || '☎️');
|
|
430
|
+
const identityNameEscaped = identityName.replace(/"/g, '\\"');
|
|
431
|
+
const identityEmojiEscaped = identityEmoji.replace(/"/g, '\\"');
|
|
432
|
+
|
|
433
|
+
const addCommand = `openclaw agents add "${escaped}" --workspace "${workspaceEscaped}"`;
|
|
434
|
+
try {
|
|
435
|
+
const out = execSync(addCommand, {
|
|
436
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
437
|
+
encoding: 'utf8',
|
|
438
|
+
});
|
|
439
|
+
openclaw.created = true;
|
|
440
|
+
openclaw.command = addCommand;
|
|
441
|
+
openclaw.steps.push({ step: 'agents add', ok: true, command: addCommand, output: String(out || '').trim() });
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const stderr = String(error?.stderr || '').trim();
|
|
444
|
+
const stdout = String(error?.stdout || '').trim();
|
|
445
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
446
|
+
if (/already exists|ya existe/i.test(combined)) {
|
|
447
|
+
openclaw.created = true;
|
|
448
|
+
openclaw.steps.push({ step: 'agents add', ok: true, command: addCommand, output: combined || 'Agent already exists' });
|
|
449
|
+
} else {
|
|
450
|
+
openclaw.steps.push({ step: 'agents add', ok: false, command: addCommand, output: combined || error.message });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (openclaw.created) {
|
|
455
|
+
const identityCommand = `openclaw agents set-identity --agent "${escaped}" --name "${identityNameEscaped}" --emoji "${identityEmojiEscaped}"`;
|
|
456
|
+
try {
|
|
457
|
+
const out = execSync(identityCommand, {
|
|
458
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
459
|
+
encoding: 'utf8',
|
|
460
|
+
});
|
|
461
|
+
openclaw.identity_set = true;
|
|
462
|
+
openclaw.steps.push({ step: 'agents set-identity', ok: true, command: identityCommand, output: String(out || '').trim() });
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const stderr = String(error?.stderr || '').trim();
|
|
465
|
+
const stdout = String(error?.stdout || '').trim();
|
|
466
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
467
|
+
openclaw.steps.push({ step: 'agents set-identity', ok: false, command: identityCommand, output: combined || error.message });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const listCommand = 'openclaw agents list';
|
|
471
|
+
try {
|
|
472
|
+
const out = execSync(listCommand, {
|
|
473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
+
encoding: 'utf8',
|
|
475
|
+
});
|
|
476
|
+
openclaw.listed = true;
|
|
477
|
+
openclaw.steps.push({ step: 'agents list', ok: true, command: listCommand, output: String(out || '').trim() });
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const stderr = String(error?.stderr || '').trim();
|
|
480
|
+
const stdout = String(error?.stdout || '').trim();
|
|
481
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
482
|
+
openclaw.steps.push({ step: 'agents list', ok: false, command: listCommand, output: combined || error.message });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (whatsapp.enabled) {
|
|
486
|
+
const accountEscaped = 'citaspro';
|
|
487
|
+
const loginCommand = `openclaw channels login --channel whatsapp --account "${accountEscaped}"`;
|
|
488
|
+
try {
|
|
489
|
+
const out = execSync(loginCommand, {
|
|
490
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
491
|
+
encoding: 'utf8',
|
|
492
|
+
});
|
|
493
|
+
openclaw.steps.push({ step: 'channels login whatsapp', ok: true, command: loginCommand, output: String(out || '').trim() });
|
|
494
|
+
} catch (error) {
|
|
495
|
+
const stderr = String(error?.stderr || '').trim();
|
|
496
|
+
const stdout = String(error?.stdout || '').trim();
|
|
497
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
498
|
+
openclaw.steps.push({ step: 'channels login whatsapp', ok: false, command: loginCommand, output: combined || error.message });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const failed = openclaw.steps.find((s) => !s.ok);
|
|
504
|
+
openclaw.details = failed ? failed.output : 'OpenClaw agent scaffold + registration OK';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
action: 'openclaw:init',
|
|
510
|
+
agent: safeName,
|
|
511
|
+
output_dir: agentDir,
|
|
512
|
+
files: ['SOUL.md', 'HEARDBEAT.md', 'CHANNELS.md', 'skills/citaspro/SKILL.md'],
|
|
513
|
+
skill_path: path.join(skillDir, 'SKILL.md'),
|
|
514
|
+
channels_path: path.join(agentDir, 'CHANNELS.md'),
|
|
515
|
+
prompt_source: '/api/prompts-and-templates',
|
|
516
|
+
prompt_preview: promptConfig.prompt,
|
|
517
|
+
tone: promptConfig.tone,
|
|
518
|
+
template: promptConfig.templateName || null,
|
|
519
|
+
whatsapp,
|
|
520
|
+
openclaw,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function printHeader(title) {
|
|
525
|
+
console.log(`\n${bold(colorize(title, 'yellow'))}`);
|
|
526
|
+
console.log(colorize('-'.repeat(title.length), 'gray'));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function printJson(data) {
|
|
530
|
+
console.log(JSON.stringify(data, null, 2));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function outputResult(data, asJson) {
|
|
534
|
+
if (asJson) {
|
|
535
|
+
printJson(data);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function extractErrorMessage(error) {
|
|
540
|
+
const responseData = error?.response?.data;
|
|
541
|
+
if (responseData?.details) {
|
|
542
|
+
return responseData.details;
|
|
543
|
+
}
|
|
544
|
+
if (responseData?.error) {
|
|
545
|
+
return responseData.error;
|
|
546
|
+
}
|
|
547
|
+
if (responseData) {
|
|
548
|
+
return JSON.stringify(responseData);
|
|
549
|
+
}
|
|
550
|
+
if (error?.message && error.message !== error?.name) {
|
|
551
|
+
return error.message;
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(error?.errors) && error.errors.length > 0) {
|
|
554
|
+
return error.errors.map((e) => e?.message || String(e)).join('; ');
|
|
555
|
+
}
|
|
556
|
+
if (error?.code) {
|
|
557
|
+
return `${error?.name || 'Error'}:${error.code}`;
|
|
558
|
+
}
|
|
559
|
+
return error?.name || String(error) || 'Comando fallido';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function printList(rows, columns) {
|
|
563
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
564
|
+
console.log('Sin resultados.');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const mapped = rows.map((row) => {
|
|
569
|
+
const out = {};
|
|
570
|
+
columns.forEach((col) => {
|
|
571
|
+
out[col.label] = row[col.key] ?? '';
|
|
572
|
+
});
|
|
573
|
+
return out;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
console.table(mapped);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function wait(ms) {
|
|
580
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function openBrowser(url) {
|
|
584
|
+
const platform = process.platform;
|
|
585
|
+
const command = platform === 'darwin'
|
|
586
|
+
? `open "${url}"`
|
|
587
|
+
: platform === 'win32'
|
|
588
|
+
? `start "" "${url}"`
|
|
589
|
+
: `xdg-open "${url}"`;
|
|
590
|
+
|
|
591
|
+
exec(command, (error) => {
|
|
592
|
+
if (error) {
|
|
593
|
+
console.log(`No se pudo abrir navegador automáticamente. Abre manualmente: ${url}`);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function loginInteractive() {
|
|
599
|
+
const code = await post('/api/device/code', { client_id: 'citaspro-cli', scope: 'tools:all' });
|
|
600
|
+
|
|
601
|
+
console.log('\nInicia sesión para vincular este CLI:\n');
|
|
602
|
+
console.log(`Código: ${code.user_code}`);
|
|
603
|
+
console.log(`URL: ${code.verification_uri_complete}`);
|
|
604
|
+
|
|
605
|
+
openBrowser(code.verification_uri_complete);
|
|
606
|
+
console.log(`\n${colorize('Esperando aprobación en navegador...', 'yellow')}\n`);
|
|
607
|
+
|
|
608
|
+
const expiresAt = Date.now() + (Number(code.expires_in || 900) * 1000);
|
|
609
|
+
const intervalMs = Number(code.interval || 5) * 1000;
|
|
610
|
+
|
|
611
|
+
while (Date.now() < expiresAt) {
|
|
612
|
+
try {
|
|
613
|
+
const token = await post('/api/device/token', {
|
|
614
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
615
|
+
device_code: code.device_code,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const session = {
|
|
619
|
+
...token,
|
|
620
|
+
api_base: API_BASE,
|
|
621
|
+
created_at: new Date().toISOString(),
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
writeSession(session);
|
|
625
|
+
console.log(colorize('Login successful. CLI autenticado.', 'green'));
|
|
626
|
+
console.log(`${colorize('Sesión guardada en:', 'gray')} ${AUTH_FILE}`);
|
|
627
|
+
return;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const apiError = error?.response?.data?.error;
|
|
630
|
+
if (apiError === 'authorization_pending' || apiError === 'slow_down') {
|
|
631
|
+
await wait(intervalMs);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (apiError === 'expired_token') {
|
|
635
|
+
throw new Error('El código expiró. Ejecuta login de nuevo.');
|
|
636
|
+
}
|
|
637
|
+
throw new Error(error?.response?.data?.error_description || error?.response?.data?.error || error.message);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
throw new Error('Timeout esperando aprobación. Ejecuta login nuevamente.');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resolveTokenAndArgs(args) {
|
|
645
|
+
if (args.length > 0 && looksLikePayload(args[0])) {
|
|
646
|
+
const session = readSession();
|
|
647
|
+
if (!session?.access_token) {
|
|
648
|
+
throw new Error('No hay sesión. Ejecuta: node tools/citas-cli.js login');
|
|
649
|
+
}
|
|
650
|
+
return { token: session.access_token, rest: args };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (args.length > 0 && looksLikeToken(args[0])) {
|
|
654
|
+
return { token: args[0], rest: args.slice(1) };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const session = readSession();
|
|
658
|
+
if (!session?.access_token) {
|
|
659
|
+
throw new Error('No hay sesión. Ejecuta: node tools/citas-cli.js login');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return { token: session.access_token, rest: args };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function main() {
|
|
666
|
+
const [, , rawCmd, ...rawArgs] = process.argv;
|
|
667
|
+
const jsonMode = rawArgs.includes('--json');
|
|
668
|
+
const openclawFlagIndex = rawArgs.indexOf('--openclaw');
|
|
669
|
+
const whatsappFlagEnabled = rawArgs.includes('--whatsapp');
|
|
670
|
+
const openclawNameFromFlag = openclawFlagIndex >= 0
|
|
671
|
+
? rawArgs[openclawFlagIndex + 1] && !rawArgs[openclawFlagIndex + 1].startsWith('--')
|
|
672
|
+
? rawArgs[openclawFlagIndex + 1]
|
|
673
|
+
: 'nombreagente'
|
|
674
|
+
: null;
|
|
675
|
+
const argsWithoutFlags = rawArgs.filter((arg, idx) => {
|
|
676
|
+
if (arg === '--json') return false;
|
|
677
|
+
if (arg === '--openclaw') return false;
|
|
678
|
+
if (arg === '--whatsapp') return false;
|
|
679
|
+
if (openclawFlagIndex >= 0 && idx === openclawFlagIndex + 1 && !arg.startsWith('--')) return false;
|
|
680
|
+
return true;
|
|
681
|
+
});
|
|
682
|
+
let cmd = rawCmd;
|
|
683
|
+
let args = argsWithoutFlags;
|
|
684
|
+
|
|
685
|
+
if (!cmd && openclawNameFromFlag) {
|
|
686
|
+
cmd = 'openclaw:init';
|
|
687
|
+
args = [openclawNameFromFlag];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
691
|
+
usage();
|
|
692
|
+
process.exit(0);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Alias layer (compatible con futuros comandos tipo "citasproctl leads ls")
|
|
696
|
+
if (cmd === 'get') {
|
|
697
|
+
cmd = 'leads:get';
|
|
698
|
+
} else if (cmd === 'ls') {
|
|
699
|
+
cmd = 'leads:list';
|
|
700
|
+
} else if (cmd === 'openclaw') {
|
|
701
|
+
cmd = 'openclaw:init';
|
|
702
|
+
} else if (cmd === 'leads' && args[0] === 'ls') {
|
|
703
|
+
cmd = 'leads:list';
|
|
704
|
+
args = args.slice(1);
|
|
705
|
+
} else if (cmd === 'leads' && args[0] === 'get') {
|
|
706
|
+
cmd = 'leads:get';
|
|
707
|
+
args = args.slice(1);
|
|
708
|
+
} else if (cmd === 'leads' && (args[0] === 'add' || args[0] === 'create')) {
|
|
709
|
+
cmd = 'leads:create';
|
|
710
|
+
args = args.slice(1);
|
|
711
|
+
} else if (cmd === 'bookings' && args[0] === 'ls') {
|
|
712
|
+
cmd = 'bookings:list';
|
|
713
|
+
args = args.slice(1);
|
|
714
|
+
} else if (cmd === 'bookings' && (args[0] === 'mv' || args[0] === 'reschedule')) {
|
|
715
|
+
cmd = 'bookings:reschedule';
|
|
716
|
+
args = args.slice(1);
|
|
717
|
+
} else if (cmd === 'bookings' && (args[0] === 'rm' || args[0] === 'cancel')) {
|
|
718
|
+
cmd = 'tools:cancel-booking';
|
|
719
|
+
args = args.slice(1);
|
|
720
|
+
} else if (cmd === 'calendar' && (args[0] === 'avail' || args[0] === 'availability')) {
|
|
721
|
+
cmd = 'tools:availability';
|
|
722
|
+
args = args.slice(1);
|
|
723
|
+
} else if (cmd === 'calendar' && args[0] === 'caldav') {
|
|
724
|
+
cmd = 'tools:caldav';
|
|
725
|
+
args = args.slice(1);
|
|
726
|
+
} else if (cmd === 'call') {
|
|
727
|
+
cmd = 'tools:call';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
if (cmd === '--openclaw') {
|
|
732
|
+
cmd = 'openclaw:init';
|
|
733
|
+
args = [args[0] || openclawNameFromFlag || 'nombreagente'];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (cmd === 'ai:manifest') {
|
|
737
|
+
printJson(AI_MANIFEST);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (cmd === 'openclaw:init') {
|
|
742
|
+
const candidate = args[0] || openclawNameFromFlag || 'nombreagente';
|
|
743
|
+
const { token } = resolveTokenAndArgs([]);
|
|
744
|
+
const data = await createOpenClawSubagent(candidate, token, {
|
|
745
|
+
whatsapp: whatsappFlagEnabled,
|
|
746
|
+
});
|
|
747
|
+
if (jsonMode) {
|
|
748
|
+
printJson(data);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
printHeader('Subagente OpenClaw creado');
|
|
752
|
+
console.log(`Agente: ${data.agent}`);
|
|
753
|
+
console.log(`Directorio: ${data.output_dir}`);
|
|
754
|
+
console.log(`Archivos: ${data.files.join(', ')}`);
|
|
755
|
+
console.log(`Prompt base: ${data.prompt_preview}`);
|
|
756
|
+
if (data.whatsapp?.enabled) {
|
|
757
|
+
console.log(`WhatsApp: enabled (account=${data.whatsapp.account}, allow_all=true)`);
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (cmd === 'login') {
|
|
763
|
+
await loginInteractive();
|
|
764
|
+
outputResult({ ok: true, message: 'login_successful', auth_file: AUTH_FILE }, jsonMode);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (cmd === 'logout') {
|
|
769
|
+
clearSession();
|
|
770
|
+
if (jsonMode) {
|
|
771
|
+
printJson({ ok: true, message: 'logout_successful' });
|
|
772
|
+
} else {
|
|
773
|
+
console.log(colorize('Sesión eliminada.', 'green'));
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (cmd === 'whoami') {
|
|
779
|
+
const { token } = resolveTokenAndArgs(args);
|
|
780
|
+
const data = await post('/api/tools/auth.whoami', {}, token);
|
|
781
|
+
if (jsonMode) {
|
|
782
|
+
printJson(data);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
printHeader('Sesion actual');
|
|
786
|
+
console.log(`Usuario: ${data?.user?.nombre || '-'}`);
|
|
787
|
+
console.log(`Email: ${data?.user?.email || '-'}`);
|
|
788
|
+
console.log(`User ID: ${data?.user?.id || '-'}`);
|
|
789
|
+
console.log(`Tenant: ${data?.tenant_id || '-'}`);
|
|
790
|
+
console.log(`Auth: ${data?.auth_type || '-'}`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (cmd === 'device:start') {
|
|
795
|
+
const data = await post('/api/device/code', { client_id: 'citaspro-cli', scope: 'tools:all' });
|
|
796
|
+
printJson(data);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (cmd === 'device:poll') {
|
|
801
|
+
const deviceCode = args[0];
|
|
802
|
+
if (!deviceCode) {
|
|
803
|
+
throw new Error('Falta device_code');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const data = await post('/api/device/token', {
|
|
807
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
808
|
+
device_code: deviceCode,
|
|
809
|
+
});
|
|
810
|
+
printJson(data);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (cmd === 'tools:availability') {
|
|
815
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
816
|
+
const [fecha, hora_inicio, hora_fin] = rest;
|
|
817
|
+
if (!fecha || !hora_inicio || !hora_fin) {
|
|
818
|
+
throw new Error('Uso: tools:availability <fecha> <hora_inicio> <hora_fin>');
|
|
819
|
+
}
|
|
820
|
+
const data = await post('/api/tools/calendar.getAvailability', { fecha, hora_inicio, hora_fin }, token);
|
|
821
|
+
if (jsonMode) {
|
|
822
|
+
printJson(data);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
printHeader('Disponibilidad');
|
|
826
|
+
console.log(`Disponible: ${data.available ? 'SI' : 'NO'}`);
|
|
827
|
+
console.log(`Festivo: ${data?.reasons?.is_holiday ? 'SI' : 'NO'}`);
|
|
828
|
+
console.log(`Jornada: ${data?.reasons?.outside_working_hours ? 'Fuera' : 'Dentro'}`);
|
|
829
|
+
console.log(`Conflictos: ${data?.reasons?.conflicts_count ?? 0}`);
|
|
830
|
+
if (Array.isArray(data.conflicts) && data.conflicts.length > 0) {
|
|
831
|
+
printList(data.conflicts, [
|
|
832
|
+
{ key: 'id', label: 'ID' },
|
|
833
|
+
{ key: 'fecha', label: 'Fecha' },
|
|
834
|
+
{ key: 'hora_inicio', label: 'Inicio' },
|
|
835
|
+
{ key: 'hora_fin', label: 'Fin' },
|
|
836
|
+
{ key: 'contacto_nombre', label: 'Contacto' },
|
|
837
|
+
]);
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (cmd === 'leads:list') {
|
|
843
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
844
|
+
const [limitRaw] = rest;
|
|
845
|
+
const limit = limitRaw ? Number(limitRaw) : 20;
|
|
846
|
+
const data = await post('/api/tools/crm.listLeads', { limit }, token);
|
|
847
|
+
if (jsonMode) {
|
|
848
|
+
printJson(data);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
printHeader(`Leads (${data.count || 0})`);
|
|
852
|
+
printList(data.leads || [], [
|
|
853
|
+
{ key: 'id', label: 'ID' },
|
|
854
|
+
{ key: 'nombre', label: 'Nombre' },
|
|
855
|
+
{ key: 'email', label: 'Email' },
|
|
856
|
+
{ key: 'telefono', label: 'Telefono' },
|
|
857
|
+
{ key: 'estado', label: 'Estado' },
|
|
858
|
+
{ key: 'fecha_creacion', label: 'Creado' },
|
|
859
|
+
]);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (cmd === 'leads:get') {
|
|
864
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
865
|
+
const [identifier] = rest;
|
|
866
|
+
if (!identifier) {
|
|
867
|
+
throw new Error('Uso: leads:get [token] <lead_id|email|phone>');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const payload = identifier.includes('@')
|
|
871
|
+
? { email: identifier }
|
|
872
|
+
: /^[0-9+\s()-]+$/.test(identifier)
|
|
873
|
+
? { telefono: identifier.replace(/\s+/g, '') }
|
|
874
|
+
: { lead_id: identifier };
|
|
875
|
+
|
|
876
|
+
const data = await post('/api/tools/crm.getLead', payload, token);
|
|
877
|
+
if (jsonMode) {
|
|
878
|
+
printJson(data);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
printHeader('Lead');
|
|
882
|
+
printJson(data.lead || data);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (cmd === 'leads:create') {
|
|
887
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
888
|
+
const [payloadRaw] = rest;
|
|
889
|
+
if (!payloadRaw) {
|
|
890
|
+
throw new Error('Uso: leads:create <json_payload>');
|
|
891
|
+
}
|
|
892
|
+
const payload = JSON.parse(payloadRaw);
|
|
893
|
+
const data = await post('/api/tools/crm.createLead', payload, token);
|
|
894
|
+
if (jsonMode) {
|
|
895
|
+
printJson(data);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
printHeader('Lead creado');
|
|
899
|
+
printJson(data.lead || data);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (cmd === 'bookings:list') {
|
|
904
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
905
|
+
const [limitRaw] = rest;
|
|
906
|
+
const limit = limitRaw ? Number(limitRaw) : 20;
|
|
907
|
+
const data = await post('/api/tools/calendar.listBookings', { limit }, token);
|
|
908
|
+
if (jsonMode) {
|
|
909
|
+
printJson(data);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
printHeader(`Reservas (${data.count || 0})`);
|
|
913
|
+
printList(data.bookings || [], [
|
|
914
|
+
{ key: 'id', label: 'ID' },
|
|
915
|
+
{ key: 'fecha', label: 'Fecha' },
|
|
916
|
+
{ key: 'hora_inicio', label: 'Inicio' },
|
|
917
|
+
{ key: 'hora_fin', label: 'Fin' },
|
|
918
|
+
{ key: 'contacto_nombre', label: 'Contacto' },
|
|
919
|
+
{ key: 'estado', label: 'Estado' },
|
|
920
|
+
]);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (cmd === 'bookings:reschedule') {
|
|
925
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
926
|
+
const [bookingId, fecha, hora_inicio, hora_fin] = rest;
|
|
927
|
+
if (!bookingId || !fecha || !hora_inicio || !hora_fin) {
|
|
928
|
+
throw new Error('Uso: bookings:reschedule <booking_id> <fecha> <hora_inicio> <hora_fin>');
|
|
929
|
+
}
|
|
930
|
+
const data = await post('/api/tools/calendar.rescheduleBooking', {
|
|
931
|
+
booking_id: bookingId,
|
|
932
|
+
fecha,
|
|
933
|
+
hora_inicio,
|
|
934
|
+
hora_fin,
|
|
935
|
+
}, token);
|
|
936
|
+
if (jsonMode) {
|
|
937
|
+
printJson(data);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
printHeader('Reserva reprogramada');
|
|
941
|
+
printJson(data.booking || data);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (cmd === 'tools:create-booking') {
|
|
946
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
947
|
+
const [payloadRaw] = rest;
|
|
948
|
+
if (!payloadRaw) {
|
|
949
|
+
throw new Error('Uso: tools:create-booking <json_payload>');
|
|
950
|
+
}
|
|
951
|
+
const payload = JSON.parse(payloadRaw);
|
|
952
|
+
const data = await post('/api/tools/calendar.createBooking', payload, token);
|
|
953
|
+
if (jsonMode) {
|
|
954
|
+
printJson(data);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
printHeader('Reserva creada');
|
|
958
|
+
printJson(data.booking || data);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (cmd === 'tools:cancel-booking') {
|
|
963
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
964
|
+
const [bookingId] = rest;
|
|
965
|
+
if (!bookingId) {
|
|
966
|
+
throw new Error('Uso: tools:cancel-booking <booking_id>');
|
|
967
|
+
}
|
|
968
|
+
const data = await post('/api/tools/calendar.cancelBooking', { booking_id: bookingId }, token);
|
|
969
|
+
if (jsonMode) {
|
|
970
|
+
printJson(data);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
printHeader('Reserva cancelada');
|
|
974
|
+
printJson(data.booking || data);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (cmd === 'tools:caldav') {
|
|
979
|
+
const { token } = resolveTokenAndArgs(args);
|
|
980
|
+
const data = await post('/api/tools/calendar.getCaldavLink', {}, token);
|
|
981
|
+
if (jsonMode) {
|
|
982
|
+
printJson(data);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
printHeader('CalDAV');
|
|
986
|
+
console.log(`URL: ${data.caldav_url || 'No disponible'}`);
|
|
987
|
+
console.log(`Usuario Radicale: ${data.radicale_username || '-'}`);
|
|
988
|
+
console.log(`Calendar ID: ${data.calendar_id || '-'}`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (cmd === 'tools:call') {
|
|
993
|
+
const { token, rest } = resolveTokenAndArgs(args);
|
|
994
|
+
const [to, message] = rest;
|
|
995
|
+
if (!to) {
|
|
996
|
+
throw new Error('Uso: tools:call <to> [message]');
|
|
997
|
+
}
|
|
998
|
+
const data = await post('/api/tools/calls.placeCall', { to, message: message || 'Llamada de prueba desde CLI' }, token);
|
|
999
|
+
if (jsonMode) {
|
|
1000
|
+
printJson(data);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
printHeader('Llamada');
|
|
1004
|
+
printJson(data);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
usage();
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
const message = extractErrorMessage(error);
|
|
1012
|
+
if (jsonMode) {
|
|
1013
|
+
printJson({ ok: false, error: message });
|
|
1014
|
+
} else {
|
|
1015
|
+
console.error(`${colorize('Error:', 'red')} ${message}`);
|
|
1016
|
+
}
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
3
|
+
"name": "citaspro-cli",
|
|
4
|
+
"version": "1.0.2",
|
|
5
|
+
"description": "CLI para gestionar leads, reservas y calendario de CitasPro",
|
|
6
|
+
"bin": {
|
|
7
|
+
"citaspro-cli": "citasproctl.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"preferGlobal": true,
|
|
11
|
+
"files": [
|
|
12
|
+
"citasproctl.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"axios": "^1.7.7",
|
|
20
|
+
"dotenv": "^16.0.3"
|
|
21
|
+
},
|
|
22
|
+
"license": "ISC"
|
|
23
|
+
}
|