nubot-lite 0.0.0-stage → 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/LICENSE +24 -0
- package/LICENSE-PANEL.md +39 -0
- package/README.md +40 -2
- package/bin/nubot-lite.mjs +100 -0
- package/lib/api.mjs +69 -0
- package/lib/args.mjs +30 -0
- package/lib/cloudflare.mjs +89 -0
- package/lib/commands/content.mjs +209 -0
- package/lib/commands/init.mjs +251 -0
- package/lib/commands/maintenance.mjs +94 -0
- package/lib/project.mjs +143 -0
- package/lib/sh.mjs +49 -0
- package/lib/ui.mjs +82 -0
- package/package.json +33 -4
- package/template/.claude/skills/nubot-lite/SKILL.md +200 -0
- package/template/LICENSE +24 -0
- package/template/gitignore +4 -0
- package/template/panel/LICENSE.md +39 -0
- package/template/panel/PANEL.md +3 -0
- package/template/panel/assets/AgendarDemoPage-DOiS40FL.js +1 -0
- package/template/panel/assets/AgentDatasetsPage-D8vQhRWO.js +2 -0
- package/template/panel/assets/AgentKnowledgeGroupsPage-DEPS1lsk.js +1 -0
- package/template/panel/assets/AgentListPage-DqY9rze5.js +1 -0
- package/template/panel/assets/AgentQaTrainingPage-Ecis7xoc.js +1 -0
- package/template/panel/assets/AiAgentWhatsappPage-DOiS40FL.js +1 -0
- package/template/panel/assets/AiMetricsPage-DEPS1lsk.js +1 -0
- package/template/panel/assets/BlogPage-DOiS40FL.js +1 -0
- package/template/panel/assets/BlogPostPage-DOiS40FL.js +1 -0
- package/template/panel/assets/CalculadoraTiempoRespuestaPage-DOiS40FL.js +1 -0
- package/template/panel/assets/CampaignDetailPage-DFMctNEj.js +1 -0
- package/template/panel/assets/CampaignsPage-CLRaXGlj.js +1 -0
- package/template/panel/assets/ChannelSelector-q9tTirNo.js +1 -0
- package/template/panel/assets/ChatbotParaWhatsappPage-DOiS40FL.js +1 -0
- package/template/panel/assets/ConfirmModal-W7VUI3Z_.js +1 -0
- package/template/panel/assets/ContactsPage-CsOFsOPw.js +14 -0
- package/template/panel/assets/CreateAgentPage-Ifxx6_UX.js +19 -0
- package/template/panel/assets/CreditLimitBanner-J49rSVEn.js +1 -0
- package/template/panel/assets/CrmParaWhatsappPage-DOiS40FL.js +1 -0
- package/template/panel/assets/DashboardPage-3Viy83dd.js +1 -0
- package/template/panel/assets/EmojiPicker-BToAe-MT.js +3 -0
- package/template/panel/assets/EventTypesSettingsPage-DCedeQzM.js +1 -0
- package/template/panel/assets/GeneradorEnlacesPage-DOiS40FL.js +1 -0
- package/template/panel/assets/GeneradorQrWhatsappPage-DOiS40FL.js +1 -0
- package/template/panel/assets/GeneralSettingsPage-B04ozcHb.js +1 -0
- package/template/panel/assets/InvitationAcceptancePage-BPgtBFl5.js +1 -0
- package/template/panel/assets/IrAlSitioPublico-DOiS40FL.js +1 -0
- package/template/panel/assets/LiteSetupPage-42rgAdnG.js +1 -0
- package/template/panel/assets/MediaProcessingService-glHGiE_o.js +1 -0
- package/template/panel/assets/NewCampaignPage-Cw99m3W8.js +1 -0
- package/template/panel/assets/OrdersConfigPage-B6BfM3lf.js +1 -0
- package/template/panel/assets/OrdersPage-Du7CJjty.js +1 -0
- package/template/panel/assets/PasswordStrengthIndicator-HZK_oU0W.js +1 -0
- package/template/panel/assets/PlantillaDetailPage-DOiS40FL.js +1 -0
- package/template/panel/assets/PlantillasIndexPage-DOiS40FL.js +1 -0
- package/template/panel/assets/PrivacyPolicy-DOiS40FL.js +1 -0
- package/template/panel/assets/ProductManagementPage-DNM7DEgt.js +21 -0
- package/template/panel/assets/PublicFormPage-DDb4OXEM.js +1 -0
- package/template/panel/assets/QaForm-BDboHNKo.js +5 -0
- package/template/panel/assets/SelectPlanPage-DALaf8E5.js +1 -0
- package/template/panel/assets/TagSelector-ChoKzvjW.js +1 -0
- package/template/panel/assets/TagsSettingsPage-DYCKveQq.js +2 -0
- package/template/panel/assets/TeamsPage-DPUcRdlh.js +1 -0
- package/template/panel/assets/WhatsappBusinessApiPage-DOiS40FL.js +1 -0
- package/template/panel/assets/WhatsappMultiagentePage-DOiS40FL.js +1 -0
- package/template/panel/assets/account-D2ltXC_U.js +1 -0
- package/template/panel/assets/admin-lEgqZTsa.js +1 -0
- package/template/panel/assets/agent.service-BcAkMU7j.js +1 -0
- package/template/panel/assets/alert-DI3GBLi1.js +1 -0
- package/template/panel/assets/alert-dialog-D8P69s-m.js +7 -0
- package/template/panel/assets/api-keys-B0UQNR5E.js +1 -0
- package/template/panel/assets/archive-LUe5bKi5.js +1 -0
- package/template/panel/assets/arrow-left-BeDdBs_Q.js +1 -0
- package/template/panel/assets/arrow-up-BiBq8Pvx.js +1 -0
- package/template/panel/assets/arrow-up-down-CaG2BONN.js +1 -0
- package/template/panel/assets/audit-BrdHcTNl.js +1 -0
- package/template/panel/assets/authService-Cbm8LdlJ.js +1 -0
- package/template/panel/assets/avatar-CILB1zwS.js +1 -0
- package/template/panel/assets/billing-IVLNopU2.js +1 -0
- package/template/panel/assets/brain-B9z7jZ7A.js +1 -0
- package/template/panel/assets/calendar-CIHhMjNy.js +1 -0
- package/template/panel/assets/calendar-days-BwJfOz8M.js +1 -0
- package/template/panel/assets/card-DcKsxjXK.js +1 -0
- package/template/panel/assets/checkbox-pcbjC0d3.js +1 -0
- package/template/panel/assets/chevrons-up-down-DC-BWTrj.js +1 -0
- package/template/panel/assets/circle-check-big-DzPo5zZX.js +1 -0
- package/template/panel/assets/circle-x-BwMKIs8b.js +1 -0
- package/template/panel/assets/code-BMAyHFJe.js +1 -0
- package/template/panel/assets/cog-M7j64wa2.js +1 -0
- package/template/panel/assets/collapsible-B22t1J-V.js +1 -0
- package/template/panel/assets/command-COdSmSIB.js +1 -0
- package/template/panel/assets/connections-Dqwd01UI.js +1 -0
- package/template/panel/assets/connectionsService-BsJf8pFz.js +3 -0
- package/template/panel/assets/contactsApi-B851NME_.js +1 -0
- package/template/panel/assets/core.esm-Dq12RbIP.js +5 -0
- package/template/panel/assets/creditErrors-CrpkwwUI.js +1 -0
- package/template/panel/assets/database-C1qvt3Py.js +1 -0
- package/template/panel/assets/datapath-C12uI_pT.js +1 -0
- package/template/panel/assets/datetime-BuZosCVk.js +1 -0
- package/template/panel/assets/download-CiVQHPeB.js +1 -0
- package/template/panel/assets/ellipsis-CMEJnUfK.js +1 -0
- package/template/panel/assets/ellipsis-vertical-DTqbEYYp.js +1 -0
- package/template/panel/assets/email-inbox-LbGeQCty.js +1 -0
- package/template/panel/assets/es-BG480iA5.js +1 -0
- package/template/panel/assets/eye-HfQCUjxJ.js +1 -0
- package/template/panel/assets/eye-off-v1Z_lKS0.js +1 -0
- package/template/panel/assets/facebook-RxMBweMB.js +1 -0
- package/template/panel/assets/file-DMzjdlTL.js +1 -0
- package/template/panel/assets/folder-D0GKakGp.js +1 -0
- package/template/panel/assets/folder-open-DQFc4qTJ.js +1 -0
- package/template/panel/assets/grip-vertical-FN-eTC2C.js +1 -0
- package/template/panel/assets/hash-BoTE56iw.js +1 -0
- package/template/panel/assets/hash.worker-BQBibGe2.js +1 -0
- package/template/panel/assets/help-center-B1MAIBWA.js +1 -0
- package/template/panel/assets/history-DhHayxUg.js +1 -0
- package/template/panel/assets/index-B183O0S5.js +1 -0
- package/template/panel/assets/index-BJCPaymI.css +1 -0
- package/template/panel/assets/index-BTxe6gLJ.js +5 -0
- package/template/panel/assets/index-BiSc3WD9.js +29 -0
- package/template/panel/assets/index-BtxJGfLL.js +6 -0
- package/template/panel/assets/index-Bz-ixwph.js +1 -0
- package/template/panel/assets/index-CCTahcpv.js +4 -0
- package/template/panel/assets/index-CEYKqxbU.js +1 -0
- package/template/panel/assets/index-CMEeV9dO.js +2 -0
- package/template/panel/assets/index-CjQSv8hF.js +2 -0
- package/template/panel/assets/index-D3vkHHjD.js +9 -0
- package/template/panel/assets/index-D4WhIkMN.js +1 -0
- package/template/panel/assets/index-DUnioqCT.js +1 -0
- package/template/panel/assets/index-DsyZ_FlA.js +61 -0
- package/template/panel/assets/index-KbFmSzBE.js +1 -0
- package/template/panel/assets/instagram-CC7iYt7V.js +1 -0
- package/template/panel/assets/interactiveLimits-CfLJ1O4_.js +1 -0
- package/template/panel/assets/landing-If3oVsS0.js +1 -0
- package/template/panel/assets/layout-grid-Cm6hfBWZ.js +1 -0
- package/template/panel/assets/list-BKc6EXnV.js +1 -0
- package/template/panel/assets/map-pin-CPmcofD3.js +1 -0
- package/template/panel/assets/maximize-2-WdbM7Kdx.js +1 -0
- package/template/panel/assets/mediaValidation-BR-ZOj5w.js +1 -0
- package/template/panel/assets/message-square-Dfn16oHc.js +1 -0
- package/template/panel/assets/meta-templates-C7yEjSOI.js +1 -0
- package/template/panel/assets/mic-CT4vyzLZ.js +1 -0
- package/template/panel/assets/minus-CyVEzdqL.js +1 -0
- package/template/panel/assets/music-Cu8AQMFp.js +1 -0
- package/template/panel/assets/page-components-o2TF52HV.js +1 -0
- package/template/panel/assets/pages-B7digtXi.js +1 -0
- package/template/panel/assets/pages-Cj5Qd1QO.js +1 -0
- package/template/panel/assets/pages-Dw7dvmaS.js +1 -0
- package/template/panel/assets/pen-line-DLnhkIuw.js +1 -0
- package/template/panel/assets/pencil-Cx8u1N8t.js +1 -0
- package/template/panel/assets/popover-MyuU-yAK.js +1 -0
- package/template/panel/assets/power-CdR2OkIX.js +1 -0
- package/template/panel/assets/product.service-CRLZOQsr.js +1 -0
- package/template/panel/assets/products.types-46xSxBS8.js +1 -0
- package/template/panel/assets/recharts-DrC5kNsx.js +36 -0
- package/template/panel/assets/save-Dua2yhWl.js +1 -0
- package/template/panel/assets/scroll-area-CavEOIyz.js +1 -0
- package/template/panel/assets/select-DlB_BbfM.js +1 -0
- package/template/panel/assets/send-Dxmdupsl.js +1 -0
- package/template/panel/assets/separator-CPT3uVRM.js +1 -0
- package/template/panel/assets/settings-2-C-c8ECWW.js +1 -0
- package/template/panel/assets/shield-alert-04pSYngI.js +1 -0
- package/template/panel/assets/skeleton-B0zh8s_y.js +1 -0
- package/template/panel/assets/slider-CY71MXOW.js +1 -0
- package/template/panel/assets/sortable.esm-CF3ooibJ.js +1 -0
- package/template/panel/assets/square-BuGqLxJB.js +1 -0
- package/template/panel/assets/square-pen-CJajoyrm.js +1 -0
- package/template/panel/assets/star-Cuegk1GF.js +1 -0
- package/template/panel/assets/storage.api-CFU3KJNp.js +20 -0
- package/template/panel/assets/switch-ODBa0fCT.js +1 -0
- package/template/panel/assets/table-CvSELOPi.js +1 -0
- package/template/panel/assets/tabs-DqXhuAr6.js +1 -0
- package/template/panel/assets/tags-By_tlh_r.js +1 -0
- package/template/panel/assets/target-DEuCdeUI.js +1 -0
- package/template/panel/assets/textarea-BYtcLsMD.js +1 -0
- package/template/panel/assets/ticket-CWba7G1i.js +1 -0
- package/template/panel/assets/trash-2-Du2vmQCH.js +1 -0
- package/template/panel/assets/trending-up-Ba4HSvuW.js +1 -0
- package/template/panel/assets/upload-C7TwpdyA.js +1 -0
- package/template/panel/assets/useAiAgentsQuery-Bm6aRHKj.js +1 -0
- package/template/panel/assets/useCountryFromLocale-BavmL9FK.js +18 -0
- package/template/panel/assets/useDebounce-Bfcb_jpD.js +1 -0
- package/template/panel/assets/useFunnelStats-tvjpmWIC.js +1 -0
- package/template/panel/assets/useInfiniteQuery-BXUM1Z9V.js +1 -0
- package/template/panel/assets/useMutation-a5tIBvQY.js +1 -0
- package/template/panel/assets/useUploadConfig-BuiF0X-3.js +1 -0
- package/template/panel/assets/user-DwCALzIG.js +1 -0
- package/template/panel/assets/user-plus-BiUjcHil.js +1 -0
- package/template/panel/assets/video-VfwKgB6z.js +1 -0
- package/template/panel/assets/volume-2-C5DMdOVq.js +1 -0
- package/template/panel/assets/xlsx-BvJTHLik.js +105 -0
- package/template/panel/favicon.ico +0 -0
- package/template/panel/index.html +20 -0
- package/template/panel/nubot-logo.png +0 -0
- package/template/panel/sounds/bell.mp3 +0 -0
- package/template/panel/sounds/chime.mp3 +0 -0
- package/template/panel/sounds/notification.mp3 +0 -0
- package/template/panel/sounds/pop.mp3 +0 -0
- package/template/panel/version.json +4 -0
- package/template/worker/migrations/0001_init.sql +75 -0
- package/template/worker/migrations/0002_users.sql +13 -0
- package/template/worker/migrations/0003_agent_data.sql +117 -0
- package/template/worker/migrations/0004_operations.sql +35 -0
- package/template/worker/migrations/0005_panel.sql +31 -0
- package/template/worker/package.json +20 -0
- package/template/worker/src/agent/dates.ts +53 -0
- package/template/worker/src/agent/prompt.ts +134 -0
- package/template/worker/src/agent/run.ts +243 -0
- package/template/worker/src/agent/sandbox.ts +113 -0
- package/template/worker/src/agent/search.ts +79 -0
- package/template/worker/src/agent/settings.ts +33 -0
- package/template/worker/src/agent/tools.ts +463 -0
- package/template/worker/src/auth/password.ts +31 -0
- package/template/worker/src/auth/session.ts +49 -0
- package/template/worker/src/channels/provider.ts +27 -0
- package/template/worker/src/channels/zernio.ts +148 -0
- package/template/worker/src/do/conversation.ts +68 -0
- package/template/worker/src/do/inbox-hub.ts +113 -0
- package/template/worker/src/do/socketio.ts +55 -0
- package/template/worker/src/env.ts +25 -0
- package/template/worker/src/http.ts +14 -0
- package/template/worker/src/inbox/contacts.ts +30 -0
- package/template/worker/src/inbox/funnel.ts +27 -0
- package/template/worker/src/inbox/store.ts +187 -0
- package/template/worker/src/index.ts +57 -0
- package/template/worker/src/media/files.ts +136 -0
- package/template/worker/src/nubot/account.ts +181 -0
- package/template/worker/src/nubot/agents.ts +367 -0
- package/template/worker/src/nubot/contacts.ts +386 -0
- package/template/worker/src/nubot/funnels.ts +670 -0
- package/template/worker/src/nubot/inbox.ts +616 -0
- package/template/worker/src/nubot/index.ts +122 -0
- package/template/worker/src/nubot/jwt.ts +44 -0
- package/template/worker/src/nubot/mappers.ts +234 -0
- package/template/worker/src/nubot/products.ts +280 -0
- package/template/worker/src/nubot/realtime.ts +42 -0
- package/template/worker/src/nubot/session.ts +68 -0
- package/template/worker/src/nubot/team.ts +259 -0
- package/template/worker/src/nubot/templates.ts +198 -0
- package/template/worker/src/nubot/uploads.ts +73 -0
- package/template/worker/src/routes/catalog.ts +108 -0
- package/template/worker/src/routes/funnels.ts +98 -0
- package/template/worker/src/routes/knowledge.ts +77 -0
- package/template/worker/src/routes/quick-replies.ts +50 -0
- package/template/worker/src/routes/requests.ts +91 -0
- package/template/worker/src/routes/settings.ts +48 -0
- package/template/worker/src/routes/system.ts +100 -0
- package/template/worker/src/routes/tags.ts +32 -0
- package/template/worker/src/webhooks/zernio.ts +125 -0
- package/template/worker/tsconfig.json +21 -0
- package/template/worker/wrangler.jsonc +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nubot
|
|
4
|
+
|
|
5
|
+
This license covers this repository except the compiled panel in apps/panel,
|
|
6
|
+
which has its own license (LICENSE-PANEL.md).
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
package/LICENSE-PANEL.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Licencia del panel de Nubot Lite
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Nubot (https://nubot.io). Todos los derechos reservados.
|
|
4
|
+
|
|
5
|
+
Esta licencia cubre el panel compilado: la carpeta `apps/panel` de este repositorio y su copia `panel/` en cada
|
|
6
|
+
instalación. No cubre el resto del repositorio, que se distribuye bajo la licencia MIT (ver `LICENSE`).
|
|
7
|
+
|
|
8
|
+
El panel no es software libre ni de código abierto, y su código fuente no se publica.
|
|
9
|
+
|
|
10
|
+
## Puedes, sin costo
|
|
11
|
+
|
|
12
|
+
- Usar el panel tal como se distribuye, junto con el servidor de Nubot Lite, para atender a tu empresa.
|
|
13
|
+
- Copiarlo dentro de tu instalación de Nubot Lite y actualizarlo con `npx nubot-lite update`.
|
|
14
|
+
- Instalar Nubot Lite con su panel para otra empresa, siempre que esa empresa lo use para sí misma bajo estas
|
|
15
|
+
mismas condiciones.
|
|
16
|
+
|
|
17
|
+
## No puedes
|
|
18
|
+
|
|
19
|
+
- Modificar, descompilar, desofuscar o hacer obras derivadas del panel, salvo lo que la ley permita expresamente.
|
|
20
|
+
- Usarlo con un servidor, backend o producto distinto de Nubot Lite.
|
|
21
|
+
- Redistribuirlo, venderlo, sublicenciarlo u ofrecerlo como servicio, por separado o dentro de otro producto,
|
|
22
|
+
fuera de lo que permite la sección anterior.
|
|
23
|
+
- Quitar o cambiar las marcas, los logos o los avisos de Nubot que contiene.
|
|
24
|
+
|
|
25
|
+
## Marcas
|
|
26
|
+
|
|
27
|
+
«Nubot» y su logo son marcas de Nubot. Esta licencia no da derecho a usarlas fuera del panel.
|
|
28
|
+
|
|
29
|
+
## Sin garantía
|
|
30
|
+
|
|
31
|
+
El panel se entrega «tal cual», sin garantías de ningún tipo. En la medida en que la ley lo permita, Nubot no
|
|
32
|
+
responde por daños derivados de su uso.
|
|
33
|
+
|
|
34
|
+
## Fin de la licencia
|
|
35
|
+
|
|
36
|
+
Esta licencia termina sola si incumples cualquiera de sus condiciones. En ese caso tienes que dejar de usar el
|
|
37
|
+
panel y borrar sus copias.
|
|
38
|
+
|
|
39
|
+
Para otros usos (varias empresas, agencias, reventa o integraciones), escribe a Nubot: https://nubot.io
|
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nubot-lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Instala **Nubot Lite** en tu cuenta de Cloudflare: una bandeja de WhatsApp con agente de IA (Claude) y embudo
|
|
4
|
+
de ventas, para una sola empresa. Corre en la capa gratuita de Cloudflare, sin tarjeta. WhatsApp entra y sale
|
|
5
|
+
por [Zernio](https://zernio.com).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx nubot-lite init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Necesitas Node.js 20 o más nuevo. Las cuentas de Cloudflare, Anthropic y Zernio se crean en el paso en que
|
|
12
|
+
hacen falta.
|
|
13
|
+
|
|
14
|
+
## Paso a paso
|
|
15
|
+
|
|
16
|
+
| Paso | Comando |
|
|
17
|
+
|---|---|
|
|
18
|
+
| 1. Levantar el CRM en Cloudflare (gratis, sin tarjeta) | `npx nubot-lite init` |
|
|
19
|
+
| 2. Conectar el agente de IA (llave de Anthropic) | `npx nubot-lite claude` |
|
|
20
|
+
| 3. Conectar WhatsApp (Zernio) | `npx nubot-lite whatsapp` |
|
|
21
|
+
| 4. Enseñarle el negocio al agente | `npx nubot-lite apply <carpeta>` |
|
|
22
|
+
|
|
23
|
+
Los pasos 2 a 4 se corren dentro de la carpeta que crea `init`. Con Claude Code puedes pedirlo con tus palabras:
|
|
24
|
+
«Instala Nubot Lite para mi empresa», «Conecta mi agente», «Conecta mi WhatsApp», «Arma el agente de mi empresa».
|
|
25
|
+
|
|
26
|
+
| Comando | Qué hace |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `npx nubot-lite doctor` | Revisa que todo funcione y dice qué falta |
|
|
29
|
+
| `npx nubot-lite update` | Trae la versión nueva sin pisar los archivos que cambiaste |
|
|
30
|
+
| `npx nubot-lite test "hola" "¿precio?"` | Conversación de prueba con el agente real, sin WhatsApp |
|
|
31
|
+
|
|
32
|
+
La guía completa está en [GitHub](https://github.com/aichain-research/nubot-lite/blob/main/docs/GUIA.md).
|
|
33
|
+
|
|
34
|
+
## Licencia
|
|
35
|
+
|
|
36
|
+
La CLI y el servidor son [MIT](https://github.com/aichain-research/nubot-lite/blob/main/LICENSE). El panel
|
|
37
|
+
compilado que se instala con ellos tiene su propia licencia
|
|
38
|
+
([LICENSE-PANEL.md](https://github.com/aichain-research/nubot-lite/blob/main/LICENSE-PANEL.md)): se usa sin costo
|
|
39
|
+
con Nubot Lite, pero no es código abierto.
|
|
40
|
+
|
|
41
|
+
¿Necesitas varios agentes, Instagram, Messenger, campañas o workflows? Eso es [Nubot](https://nubot.io).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "../lib/args.mjs";
|
|
3
|
+
import { CliError, red } from "../lib/ui.mjs";
|
|
4
|
+
import { init } from "../lib/commands/init.mjs";
|
|
5
|
+
import { claude, doctor, update, whatsapp } from "../lib/commands/maintenance.mjs";
|
|
6
|
+
import {
|
|
7
|
+
addKnowledge,
|
|
8
|
+
applyAgent,
|
|
9
|
+
applyFolder,
|
|
10
|
+
applyFunnel,
|
|
11
|
+
applyQuickReplies,
|
|
12
|
+
applyRequestTypes,
|
|
13
|
+
applyTags,
|
|
14
|
+
importCatalog,
|
|
15
|
+
testAgent,
|
|
16
|
+
} from "../lib/commands/content.mjs";
|
|
17
|
+
|
|
18
|
+
const HELP = `Nubot Lite: bandeja de WhatsApp con agente de IA en tu cuenta de Cloudflare.
|
|
19
|
+
|
|
20
|
+
Paso a paso
|
|
21
|
+
npx nubot-lite init 1. Levanta tu CRM en Cloudflare y te da el link
|
|
22
|
+
--business "Casa Norte" --dir ./carpeta --timezone America/Lima --yes
|
|
23
|
+
npx nubot-lite claude 2. Conecta el agente de IA (pide tu llave de Anthropic y la prueba)
|
|
24
|
+
--model claude-sonnet-5-5 --anthropic-key ...
|
|
25
|
+
npx nubot-lite whatsapp 3. Conecta tu número de WhatsApp por Zernio
|
|
26
|
+
--zernio-key ...
|
|
27
|
+
|
|
28
|
+
Mantener
|
|
29
|
+
npx nubot-lite doctor Revisa que todo funcione y dice qué falta
|
|
30
|
+
npx nubot-lite update Trae la versión nueva sin pisar tus cambios (--force para pisarlos)
|
|
31
|
+
|
|
32
|
+
Armar el agente (desde la carpeta de la instalación)
|
|
33
|
+
npx nubot-lite apply <carpeta> Aplica una carpeta completa (agente, embudo, catálogo, conocimiento…)
|
|
34
|
+
npx nubot-lite agent <agente.json>
|
|
35
|
+
npx nubot-lite funnel <embudo.json>
|
|
36
|
+
npx nubot-lite tags <etiquetas.json>
|
|
37
|
+
npx nubot-lite requests <solicitudes.json>
|
|
38
|
+
npx nubot-lite quick-replies <respuestas.json>
|
|
39
|
+
npx nubot-lite catalog <catalogo.json> --media <carpeta de fotos>
|
|
40
|
+
npx nubot-lite knowledge <archivo.md|carpeta>...
|
|
41
|
+
npx nubot-lite test "mensaje 1" "mensaje 2" Conversación de prueba con el agente real (--file, --json)
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
46
|
+
const { positional, flags } = parseArgs(rest);
|
|
47
|
+
const need = (n, usage) => {
|
|
48
|
+
if (positional.length < n) throw new CliError(`Uso: npx nubot-lite ${usage}`);
|
|
49
|
+
};
|
|
50
|
+
switch (command) {
|
|
51
|
+
case "init":
|
|
52
|
+
return init(flags);
|
|
53
|
+
case "doctor":
|
|
54
|
+
return doctor();
|
|
55
|
+
case "update":
|
|
56
|
+
return update(flags);
|
|
57
|
+
case "claude":
|
|
58
|
+
return claude(flags);
|
|
59
|
+
case "whatsapp":
|
|
60
|
+
return whatsapp(flags);
|
|
61
|
+
case "apply":
|
|
62
|
+
need(1, "apply <carpeta>");
|
|
63
|
+
return applyFolder(positional[0]);
|
|
64
|
+
case "agent":
|
|
65
|
+
need(1, "agent <agente.json>");
|
|
66
|
+
return applyAgent(positional[0]);
|
|
67
|
+
case "funnel":
|
|
68
|
+
need(1, "funnel <embudo.json>");
|
|
69
|
+
return applyFunnel(positional[0]);
|
|
70
|
+
case "tags":
|
|
71
|
+
need(1, "tags <etiquetas.json>");
|
|
72
|
+
return applyTags(positional[0]);
|
|
73
|
+
case "requests":
|
|
74
|
+
need(1, "requests <solicitudes.json>");
|
|
75
|
+
return applyRequestTypes(positional[0]);
|
|
76
|
+
case "quick-replies":
|
|
77
|
+
need(1, "quick-replies <respuestas.json>");
|
|
78
|
+
return applyQuickReplies(positional[0]);
|
|
79
|
+
case "catalog":
|
|
80
|
+
need(1, "catalog <catalogo.json> --media <carpeta>");
|
|
81
|
+
return importCatalog(positional[0], flags.media === true ? undefined : flags.media);
|
|
82
|
+
case "knowledge":
|
|
83
|
+
need(1, "knowledge <archivo.md|carpeta>...");
|
|
84
|
+
return addKnowledge(positional);
|
|
85
|
+
case "test":
|
|
86
|
+
return testAgent(positional, flags);
|
|
87
|
+
case undefined:
|
|
88
|
+
case "help":
|
|
89
|
+
case "--help":
|
|
90
|
+
console.log(HELP);
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
throw new CliError(`No conozco el comando "${command}". Mira la ayuda: npx nubot-lite help`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch((err) => {
|
|
98
|
+
console.error(`\n${red("✗")} ${err instanceof CliError ? err.message : err.stack ?? err}`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CliError } from "./ui.mjs";
|
|
2
|
+
|
|
3
|
+
/** Cliente de la API del Worker con la llave de admin de esta instalación. */
|
|
4
|
+
export function apiClient({ url, adminToken }) {
|
|
5
|
+
async function call(method, path, body, { form = false } = {}) {
|
|
6
|
+
let res;
|
|
7
|
+
try {
|
|
8
|
+
res = await fetch(`${url}${path}`, {
|
|
9
|
+
method,
|
|
10
|
+
headers: { Authorization: `Bearer ${adminToken}`, ...(form || body === undefined ? {} : { "Content-Type": "application/json" }) },
|
|
11
|
+
body: form ? body : body === undefined ? undefined : JSON.stringify(body),
|
|
12
|
+
});
|
|
13
|
+
} catch (err) {
|
|
14
|
+
throw new CliError(`No pude conectar con ${url}: ${err.message}`);
|
|
15
|
+
}
|
|
16
|
+
const data = await res.json().catch(() => ({}));
|
|
17
|
+
if (!res.ok) throw new CliError(data.error ?? `${method} ${path} respondió ${res.status}`);
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
get: (path) => call("GET", path),
|
|
22
|
+
post: (path, body) => call("POST", path, body),
|
|
23
|
+
put: (path, body) => call("PUT", path, body),
|
|
24
|
+
patch: (path, body) => call("PATCH", path, body),
|
|
25
|
+
del: (path) => call("DELETE", path),
|
|
26
|
+
upload: (path, form) => call("POST", path, form, { form: true }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ZERNIO_API = "https://zernio.com/api";
|
|
31
|
+
|
|
32
|
+
export const WEBHOOK_EVENTS = ["message.received", "message.sent", "message.delivered", "message.read", "message.failed"];
|
|
33
|
+
|
|
34
|
+
async function zernio(key, method, path, body) {
|
|
35
|
+
const res = await fetch(`${ZERNIO_API}${path}`, {
|
|
36
|
+
method,
|
|
37
|
+
headers: { Authorization: `Bearer ${key}`, ...(body ? { "Content-Type": "application/json" } : {}) },
|
|
38
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
const data = await res.json().catch(() => ({}));
|
|
41
|
+
if (res.status === 401) throw new CliError("La API key de Zernio no es válida.");
|
|
42
|
+
if (!res.ok) throw new CliError(`Zernio respondió ${res.status}: ${data.error ?? data.message ?? ""}`);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function whatsappAccounts(key) {
|
|
47
|
+
const data = await zernio(key, "GET", "/v1/accounts?platform=whatsapp");
|
|
48
|
+
const accounts = (data.accounts ?? []).map((a) => ({ id: a._id ?? a.id ?? a.accountId, name: a.displayName ?? a.username ?? "WhatsApp", active: a.isActive !== false }));
|
|
49
|
+
// El número de prueba compartido de Zernio no está entre las cuentas: sale en sus números.
|
|
50
|
+
const { sandbox } = await zernio(key, "GET", "/v1/phone-numbers").catch(() => ({}));
|
|
51
|
+
if (sandbox?.accountId && !accounts.some((a) => a.id === sandbox.accountId)) {
|
|
52
|
+
accounts.push({ id: sandbox.accountId, name: `Sandbox de Zernio (${sandbox.phoneNumber}, solo para probar)`, active: true });
|
|
53
|
+
}
|
|
54
|
+
return accounts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registra (o actualiza) el webhook que lleva los mensajes de WhatsApp al Worker. */
|
|
58
|
+
export async function ensureWebhook(key, { url, secret, accountId }) {
|
|
59
|
+
const target = `${url}/webhooks/zernio`;
|
|
60
|
+
const { webhooks = [] } = await zernio(key, "GET", "/v1/webhooks/settings");
|
|
61
|
+
const body = { name: "Nubot Lite", url: target, secret, events: WEBHOOK_EVENTS, isActive: true, accountIds: accountId ? [accountId] : [] };
|
|
62
|
+
const existing = webhooks.find((w) => w.url === target);
|
|
63
|
+
if (existing) {
|
|
64
|
+
await zernio(key, "PUT", "/v1/webhooks/settings", { _id: existing._id, ...body });
|
|
65
|
+
return "updated";
|
|
66
|
+
}
|
|
67
|
+
await zernio(key, "POST", "/v1/webhooks/settings", body);
|
|
68
|
+
return "created";
|
|
69
|
+
}
|
package/lib/args.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** `--clave valor`, `--clave=valor` y `--bandera`; lo demás son posicionales. */
|
|
2
|
+
export function parseArgs(argv) {
|
|
3
|
+
const positional = [];
|
|
4
|
+
const flags = {};
|
|
5
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6
|
+
const arg = argv[i];
|
|
7
|
+
if (!arg.startsWith("--")) {
|
|
8
|
+
positional.push(arg);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const [key, inline] = arg.slice(2).split(/=(.*)/s);
|
|
12
|
+
const name = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
13
|
+
if (inline !== undefined) flags[name] = inline;
|
|
14
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) flags[name] = argv[++i];
|
|
15
|
+
else flags[name] = true;
|
|
16
|
+
}
|
|
17
|
+
return { positional, flags };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function slugify(text) {
|
|
21
|
+
return (
|
|
22
|
+
text
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.normalize("NFD")
|
|
25
|
+
.replace(/[̀-ͯ]/g, "")
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "")
|
|
28
|
+
.slice(0, 40) || "empresa"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { wrangler } from "./sh.mjs";
|
|
4
|
+
import { CliError, closePrompts, info } from "./ui.mjs";
|
|
5
|
+
|
|
6
|
+
export function parseD1Id(output) {
|
|
7
|
+
return output.match(/"database_id":\s*"([0-9a-f-]{36})"/)?.[1] ?? null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseKvId(output) {
|
|
11
|
+
return output.match(/"?id"?\s*[:=]\s*"([0-9a-f]{32})"/)?.[1] ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseWorkerUrl(output) {
|
|
15
|
+
return output.match(/https:\/\/[a-z0-9-]+\.[a-z0-9-]+\.workers\.dev/)?.[0] ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function ensureLogin(workerDir) {
|
|
19
|
+
const who = await wrangler(workerDir, ["whoami"], { capture: true });
|
|
20
|
+
if (who.code === 0 && !/not authenticated/i.test(who.out)) return;
|
|
21
|
+
info("Se abrirá el navegador para entrar a tu cuenta de Cloudflare (es gratis y no pide tarjeta).");
|
|
22
|
+
// El login usa la terminal directamente: suelto el teclado mientras tanto.
|
|
23
|
+
closePrompts();
|
|
24
|
+
const login = await wrangler(workerDir, ["login"], { interactive: true });
|
|
25
|
+
if (login.code !== 0) throw new CliError("No se pudo entrar a Cloudflare. Vuelve a intentar con: npx nubot-lite init");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function createD1(workerDir, name) {
|
|
29
|
+
const r = await wrangler(workerDir, ["d1", "create", name], { capture: true });
|
|
30
|
+
const id = parseD1Id(r.out);
|
|
31
|
+
if (id) return id;
|
|
32
|
+
if (/already exists/i.test(r.out)) {
|
|
33
|
+
const list = await wrangler(workerDir, ["d1", "list", "--json"], { capture: true });
|
|
34
|
+
const found = JSON.parse(list.out.slice(list.out.indexOf("["))).find((db) => db.name === name);
|
|
35
|
+
if (found) return found.uuid;
|
|
36
|
+
}
|
|
37
|
+
throw new CliError(`No se pudo crear la base de datos D1.\n${r.out}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function createKv(workerDir, title) {
|
|
41
|
+
const r = await wrangler(workerDir, ["kv", "namespace", "create", title], { capture: true });
|
|
42
|
+
const id = parseKvId(r.out);
|
|
43
|
+
if (id) return id;
|
|
44
|
+
if (/already exists/i.test(r.out)) {
|
|
45
|
+
const list = await wrangler(workerDir, ["kv", "namespace", "list"], { capture: true });
|
|
46
|
+
const found = JSON.parse(list.out.slice(list.out.indexOf("["))).find((ns) => ns.title === title || ns.title.endsWith(title));
|
|
47
|
+
if (found) return found.id;
|
|
48
|
+
}
|
|
49
|
+
throw new CliError(`No se pudo crear el espacio de archivos (KV).\n${r.out}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function migrate(workerDir) {
|
|
53
|
+
const r = await wrangler(workerDir, ["d1", "migrations", "apply", "DB", "--remote"], { capture: true, env: { CI: "1" } });
|
|
54
|
+
if (r.code !== 0) throw new CliError(`Fallaron las migraciones de la base de datos.\n${r.out}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function deploy(workerDir) {
|
|
58
|
+
let r = await wrangler(workerDir, ["deploy"], { capture: true });
|
|
59
|
+
if (r.code !== 0 && /workers\.dev subdomain/i.test(r.out)) {
|
|
60
|
+
// Cuenta nueva sin subdominio: Wrangler lo pide en la terminal y lo registra.
|
|
61
|
+
info("Tu cuenta todavía no tiene una dirección *.workers.dev. Cloudflare te va a pedir un nombre corto para crearla.");
|
|
62
|
+
closePrompts();
|
|
63
|
+
await wrangler(workerDir, ["deploy"], { interactive: true });
|
|
64
|
+
r = await wrangler(workerDir, ["deploy"], { capture: true });
|
|
65
|
+
}
|
|
66
|
+
if (r.code !== 0) {
|
|
67
|
+
if (/workers\.dev subdomain/i.test(r.out)) {
|
|
68
|
+
throw new CliError(
|
|
69
|
+
"Tu cuenta de Cloudflare no tiene una dirección *.workers.dev. Abre dash.cloudflare.com → Workers & Pages (con entrar se crea) y vuelve a correr el mismo comando.",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw new CliError(`No se pudo publicar en Cloudflare. Esto respondió Cloudflare:\n${r.out.trim().split("\n").slice(-15).join("\n")}`);
|
|
73
|
+
}
|
|
74
|
+
const url = parseWorkerUrl(r.out);
|
|
75
|
+
if (!url) throw new CliError(`Se publicó, pero no encontré la dirección en la salida de Wrangler:\n${r.out}`);
|
|
76
|
+
return url;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Carga los secretos de una vez. El archivo temporal se borra aunque algo falle. */
|
|
80
|
+
export async function putSecrets(workerDir, secrets) {
|
|
81
|
+
const file = join(workerDir, ".secrets.tmp.json");
|
|
82
|
+
writeFileSync(file, JSON.stringify(secrets));
|
|
83
|
+
try {
|
|
84
|
+
const r = await wrangler(workerDir, ["secret", "bulk", ".secrets.tmp.json"], { capture: true });
|
|
85
|
+
if (r.code !== 0) throw new CliError(`No se pudieron guardar los secretos.\n${r.out}`);
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(file, { force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Comandos para cargar el agente y su contenido. Los usa la skill de Claude Code:
|
|
2
|
+
// Claude arma archivos en la carpeta de la empresa y los aplica con estos comandos.
|
|
3
|
+
// Todos se pueden repetir: actualizan lo que ya existe en vez de duplicarlo.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
7
|
+
import { apiClient } from "../api.mjs";
|
|
8
|
+
import { readState, requireProject } from "../project.mjs";
|
|
9
|
+
import { bold, CliError, dim, info, ok, step, warn } from "../ui.mjs";
|
|
10
|
+
|
|
11
|
+
const IMAGE_TYPES = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif" };
|
|
12
|
+
|
|
13
|
+
function api() {
|
|
14
|
+
const dir = requireProject();
|
|
15
|
+
const config = readState(dir, "config.json");
|
|
16
|
+
if (!config?.url) throw new CliError("La instalación quedó a medias. Termínala con: npx nubot-lite init --dir .");
|
|
17
|
+
return apiClient(config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJson(file) {
|
|
21
|
+
const path = resolve(file);
|
|
22
|
+
if (!existsSync(path)) throw new CliError(`No existe el archivo ${path}`);
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new CliError(`${basename(path)} no es un JSON válido: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const norm = (s) => String(s ?? "").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").trim();
|
|
31
|
+
|
|
32
|
+
/** agente.json: { businessName, agentName, agentRole, instructions, timezone, model } */
|
|
33
|
+
export async function applyAgent(file, client = api()) {
|
|
34
|
+
const data = readJson(file);
|
|
35
|
+
const { settings } = await client.put("/api/settings/agent", data);
|
|
36
|
+
ok(`Agente: ${settings.agentName} (${settings.model})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** embudo.json: { name, stages: ["Nuevo", ...], default: true } o una lista de esos. */
|
|
40
|
+
export async function applyFunnel(file, client = api()) {
|
|
41
|
+
const data = readJson(file);
|
|
42
|
+
for (const f of Array.isArray(data) ? data : [data]) {
|
|
43
|
+
if (!f.name || !Array.isArray(f.stages) || !f.stages.length) throw new CliError("Cada embudo necesita name y stages.");
|
|
44
|
+
const { funnels } = await client.get("/api/funnels");
|
|
45
|
+
let funnel = funnels.find((x) => norm(x.name) === norm(f.name));
|
|
46
|
+
if (!funnel) {
|
|
47
|
+
const { funnel: created } = await client.post("/api/funnels", { name: f.name, stages: f.stages });
|
|
48
|
+
ok(`Embudo ${f.name} creado con ${f.stages.length} etapas`);
|
|
49
|
+
funnel = { id: created.id, stages: [] };
|
|
50
|
+
} else {
|
|
51
|
+
const have = new Set(funnel.stages.map((s) => norm(s.name)));
|
|
52
|
+
for (const s of f.stages.filter((s) => !have.has(norm(s)))) await client.post(`/api/funnels/${funnel.id}/stages`, { name: s });
|
|
53
|
+
const { funnels: fresh } = await client.get("/api/funnels");
|
|
54
|
+
const current = fresh.find((x) => x.id === funnel.id).stages;
|
|
55
|
+
const order = f.stages.map((s) => current.find((c) => norm(c.name) === norm(s))?.id).filter(Boolean);
|
|
56
|
+
// Las etapas que ya no están en el archivo se borran si no tienen conversaciones; si tienen, se quedan al final.
|
|
57
|
+
const kept = [];
|
|
58
|
+
for (const extra of current.filter((c) => !order.includes(c.id))) {
|
|
59
|
+
try {
|
|
60
|
+
await client.del(`/api/funnels/${funnel.id}/stages/${extra.id}`);
|
|
61
|
+
} catch {
|
|
62
|
+
kept.push(extra);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await client.put(`/api/funnels/${funnel.id}/stages/order`, { stageIds: [...order, ...kept.map((c) => c.id)] });
|
|
66
|
+
ok(`Embudo ${f.name} actualizado`);
|
|
67
|
+
if (kept.length) warn(`Quedaron etapas que no están en el archivo porque tienen conversaciones: ${kept.map((c) => c.name).join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
if (f.default) await client.put(`/api/funnels/${funnel.id}`, { isDefault: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** etiquetas.json: ["Compra", "Alquiler", ...] */
|
|
74
|
+
export async function applyTags(file, client = api()) {
|
|
75
|
+
const names = readJson(file);
|
|
76
|
+
const { tags } = await client.get("/api/tags");
|
|
77
|
+
const have = new Set(tags.map((t) => norm(t.name)));
|
|
78
|
+
let created = 0;
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
if (have.has(norm(name))) continue;
|
|
81
|
+
await client.post("/api/tags", { name });
|
|
82
|
+
created++;
|
|
83
|
+
}
|
|
84
|
+
ok(`Etiquetas: ${created} nuevas, ${names.length - created} ya existían`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** respuestas.json: [{ shortcut, title, content }] */
|
|
88
|
+
export async function applyQuickReplies(file, client = api()) {
|
|
89
|
+
const list = readJson(file);
|
|
90
|
+
const { quickReplies } = await client.get("/api/quick-replies");
|
|
91
|
+
for (const q of list) {
|
|
92
|
+
const existing = quickReplies.find((x) => x.shortcut === String(q.shortcut).replace(/^\/+/, "").toLowerCase());
|
|
93
|
+
if (existing) await client.put(`/api/quick-replies/${existing.id}`, q);
|
|
94
|
+
else await client.post("/api/quick-replies", q);
|
|
95
|
+
}
|
|
96
|
+
ok(`Respuestas rápidas: ${list.length}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** solicitudes.json: [{ name, description, fields: [{ label, required }] }] */
|
|
100
|
+
export async function applyRequestTypes(file, client = api()) {
|
|
101
|
+
const list = readJson(file);
|
|
102
|
+
const { types } = await client.get("/api/requests/types");
|
|
103
|
+
for (const t of list) {
|
|
104
|
+
const existing = types.find((x) => norm(x.name) === norm(t.name));
|
|
105
|
+
if (existing) await client.put(`/api/requests/types/${existing.id}`, t);
|
|
106
|
+
else await client.post("/api/requests/types", t);
|
|
107
|
+
}
|
|
108
|
+
ok(`Tipos de solicitud: ${list.map((t) => t.name).join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* catalogo.json: [{ name, description, price, currency, externalId, images: ["depa-surco-1.jpg"] }]
|
|
113
|
+
* Las fotos se buscan en la carpeta --media. Solo se suben a productos que todavía no tienen fotos.
|
|
114
|
+
*/
|
|
115
|
+
export async function importCatalog(file, mediaDir, client = api()) {
|
|
116
|
+
const list = readJson(file);
|
|
117
|
+
const { products } = await client.get("/api/products");
|
|
118
|
+
let created = 0;
|
|
119
|
+
let updated = 0;
|
|
120
|
+
let photos = 0;
|
|
121
|
+
for (const p of list) {
|
|
122
|
+
const existing = products.find((x) => (p.externalId && x.external_id === String(p.externalId)) || norm(x.name) === norm(p.name));
|
|
123
|
+
const body = { name: p.name, description: p.description ?? "", price: p.price ?? null, currency: p.currency ?? "PEN", externalId: p.externalId ?? null, active: p.active !== false };
|
|
124
|
+
let id;
|
|
125
|
+
if (existing) {
|
|
126
|
+
await client.put(`/api/products/${existing.id}`, body);
|
|
127
|
+
id = existing.id;
|
|
128
|
+
updated++;
|
|
129
|
+
} else {
|
|
130
|
+
id = (await client.post("/api/products", body)).product.id;
|
|
131
|
+
created++;
|
|
132
|
+
}
|
|
133
|
+
const hasImages = existing && JSON.parse(existing.images || "[]").length > 0;
|
|
134
|
+
if (!hasImages && Array.isArray(p.images) && p.images.length) {
|
|
135
|
+
if (!mediaDir) throw new CliError("El catálogo trae fotos: indica la carpeta con --media");
|
|
136
|
+
const form = new FormData();
|
|
137
|
+
for (const name of p.images.slice(0, 10)) {
|
|
138
|
+
const path = resolve(mediaDir, name);
|
|
139
|
+
const type = IMAGE_TYPES[extname(path).toLowerCase()];
|
|
140
|
+
if (!existsSync(path) || !type) {
|
|
141
|
+
warn(`${p.name}: no encontré la foto ${name} (jpg, png, webp o gif)`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
form.append("file", new Blob([readFileSync(path)], { type }), basename(path));
|
|
145
|
+
photos++;
|
|
146
|
+
}
|
|
147
|
+
if (form.has("file")) await client.upload(`/api/products/${id}/images`, form);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ok(`Catálogo: ${created} nuevos, ${updated} actualizados, ${photos} fotos subidas`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Cada .md o .txt es un documento; el título es el primer encabezado o el nombre del archivo. */
|
|
154
|
+
export async function addKnowledge(paths, client = api()) {
|
|
155
|
+
const files = paths.flatMap((p) => {
|
|
156
|
+
const full = resolve(p);
|
|
157
|
+
if (!existsSync(full)) throw new CliError(`No existe ${full}`);
|
|
158
|
+
return statSync(full).isDirectory() ? readdirSync(full).filter((f) => /\.(md|txt)$/i.test(f)).map((f) => join(full, f)) : [full];
|
|
159
|
+
});
|
|
160
|
+
const { docs } = await client.get("/api/knowledge");
|
|
161
|
+
for (const path of files) {
|
|
162
|
+
const text = readFileSync(path, "utf8").trim();
|
|
163
|
+
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
164
|
+
const title = heading ?? basename(path).replace(/\.(md|txt)$/i, "").replace(/[-_]+/g, " ");
|
|
165
|
+
const existing = docs.find((d) => norm(d.title) === norm(title));
|
|
166
|
+
if (existing) await client.put(`/api/knowledge/${existing.id}`, { title, content: text });
|
|
167
|
+
else await client.post("/api/knowledge", { title, content: text });
|
|
168
|
+
ok(`${existing ? "Actualizado" : "Agregado"}: ${title}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Aplica una carpeta completa con el formato de un agente (también el de los Giros):
|
|
174
|
+
* agente.json, embudo.json, etiquetas.json, solicitudes.json, respuestas.json,
|
|
175
|
+
* catalogo.json con fotos/, y conocimiento/ con archivos .md.
|
|
176
|
+
*/
|
|
177
|
+
export async function applyFolder(folder) {
|
|
178
|
+
const dir = resolve(folder);
|
|
179
|
+
if (!existsSync(dir)) throw new CliError(`No existe la carpeta ${dir}`);
|
|
180
|
+
const client = api();
|
|
181
|
+
const has = (name) => existsSync(join(dir, name));
|
|
182
|
+
step(`Aplicando ${dir}`);
|
|
183
|
+
if (has("agente.json")) await applyAgent(join(dir, "agente.json"), client);
|
|
184
|
+
if (has("etiquetas.json")) await applyTags(join(dir, "etiquetas.json"), client);
|
|
185
|
+
if (has("embudo.json")) await applyFunnel(join(dir, "embudo.json"), client);
|
|
186
|
+
if (has("solicitudes.json")) await applyRequestTypes(join(dir, "solicitudes.json"), client);
|
|
187
|
+
if (has("respuestas.json")) await applyQuickReplies(join(dir, "respuestas.json"), client);
|
|
188
|
+
if (has("catalogo.json")) await importCatalog(join(dir, "catalogo.json"), has("fotos") ? join(dir, "fotos") : undefined, client);
|
|
189
|
+
if (has("conocimiento")) await addKnowledge([join(dir, "conocimiento")], client);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Conversación de prueba con el agente real, sin WhatsApp. Cada argumento es un mensaje del cliente. */
|
|
193
|
+
export async function testAgent(messages, flags) {
|
|
194
|
+
const turns = flags.file ? readFileSync(resolve(flags.file), "utf8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : messages;
|
|
195
|
+
if (!turns.length) throw new CliError('Escribe al menos un mensaje: npx nubot-lite test "Hola, ¿tienen depas en Surco?"');
|
|
196
|
+
const result = await api().post("/api/system/agent/test", { messages: turns });
|
|
197
|
+
if (flags.json) {
|
|
198
|
+
console.log(JSON.stringify(result, null, 2));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const t of result.turns) {
|
|
202
|
+
console.log(`\n${bold("Cliente:")} ${t.customer}`);
|
|
203
|
+
for (const tool of t.tools) console.log(dim(` ↳ ${tool.tool}(${JSON.stringify(tool.input)}) ${tool.isError ? "✗" : "→"} ${String(tool.result).slice(0, 160)}`));
|
|
204
|
+
for (const r of t.replies) console.log(`${bold("Agente:")} ${r}`);
|
|
205
|
+
if (!t.replies.length) console.log(dim(` (sin respuesta: ${t.outcome})`));
|
|
206
|
+
}
|
|
207
|
+
if (result.usage?.errors) console.log(`\n Error de la IA: ${result.usage.errors}`);
|
|
208
|
+
if (result.usage) info(`\nTokens: ${result.usage.input ?? 0} de entrada, ${result.usage.output ?? 0} de salida, ${result.usage.cached ?? 0} desde caché`);
|
|
209
|
+
}
|