@saulwade/swl-ses 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +238 -0
- package/README.md +560 -0
- package/_userland/agentes/.gitkeep +0 -0
- package/_userland/habilidades/.gitkeep +0 -0
- package/agentes/.evolved.json +9 -0
- package/agentes/accesibilidad-wcag-swl.md +692 -0
- package/agentes/arquitecto-swl.md +238 -0
- package/agentes/auto-evolucion-swl.md +854 -0
- package/agentes/backend-api-swl.md +470 -0
- package/agentes/backend-csharp-swl.md +418 -0
- package/agentes/backend-go-swl.md +388 -0
- package/agentes/backend-java-swl.md +279 -0
- package/agentes/backend-node-swl.md +477 -0
- package/agentes/backend-python-swl.md +608 -0
- package/agentes/backend-rust-swl.md +362 -0
- package/agentes/backend-workers-swl.md +480 -0
- package/agentes/cloud-infra-swl.md +485 -0
- package/agentes/consolidador-swl.md +539 -0
- package/agentes/datos-swl.md +584 -0
- package/agentes/depurador-swl.md +349 -0
- package/agentes/devops-ci-swl.md +374 -0
- package/agentes/disenador-ui-swl.md +558 -0
- package/agentes/documentador-swl.md +343 -0
- package/agentes/evals/arquitecto-swl.evals.json +56 -0
- package/agentes/evals/auto-evolucion-swl.evals.json +68 -0
- package/agentes/evals/implementador-swl.evals.json +56 -0
- package/agentes/evals/orquestador-swl.evals.json +60 -0
- package/agentes/evals/perfilador-usuario-swl.evals.json +60 -0
- package/agentes/evals/red-team-swl.evals.json +59 -0
- package/agentes/evals/revisor-codigo-swl.evals.json +59 -0
- package/agentes/frontend-angular-swl.md +627 -0
- package/agentes/frontend-css-swl.md +720 -0
- package/agentes/frontend-react-swl.md +696 -0
- package/agentes/frontend-swl.md +500 -0
- package/agentes/frontend-tailwind-swl.md +830 -0
- package/agentes/implementador-swl.md +328 -0
- package/agentes/investigador-swl.md +430 -0
- package/agentes/investigador-ux-swl.md +500 -0
- package/agentes/llm-apps-swl.md +276 -0
- package/agentes/migrador-swl.md +417 -0
- package/agentes/mobile-android-swl.md +509 -0
- package/agentes/mobile-cross-swl.md +539 -0
- package/agentes/mobile-ios-swl.md +500 -0
- package/agentes/mobile-testing-swl.md +300 -0
- package/agentes/notificador-swl.md +916 -0
- package/agentes/observabilidad-swl.md +436 -0
- package/agentes/orquestador-swl.md +884 -0
- package/agentes/pagos-swl.md +283 -0
- package/agentes/perfilador-usuario-swl.md +306 -0
- package/agentes/planificador-swl.md +402 -0
- package/agentes/producto-prd-swl.md +587 -0
- package/agentes/red-team-swl.md +216 -0
- package/agentes/release-manager-swl.md +568 -0
- package/agentes/rendimiento-swl.md +714 -0
- package/agentes/resolutor-build-swl.md +243 -0
- package/agentes/revisor-angular-swl.md +276 -0
- package/agentes/revisor-codigo-swl.md +348 -0
- package/agentes/revisor-csharp-swl.md +262 -0
- package/agentes/revisor-go-swl.md +257 -0
- package/agentes/revisor-java-swl.md +255 -0
- package/agentes/revisor-kotlin-swl.md +271 -0
- package/agentes/revisor-nextjs-swl.md +279 -0
- package/agentes/revisor-php-swl.md +269 -0
- package/agentes/revisor-react-swl.md +276 -0
- package/agentes/revisor-rust-swl.md +344 -0
- package/agentes/revisor-seguridad-swl.md +390 -0
- package/agentes/revisor-swift-swl.md +266 -0
- package/agentes/revisor-typescript-swl.md +344 -0
- package/agentes/sre-swl.md +265 -0
- package/agentes/tdd-qa-swl.md +354 -0
- package/agentes/ux-disenador-swl.md +501 -0
- package/bin/lib/bot-comandos.js +1030 -0
- package/bin/lib/bot-discovery.js +182 -0
- package/bin/lib/bot-git.js +142 -0
- package/bin/swl-ses.js +325 -0
- package/bin/swl-telegram-bot.js +442 -0
- package/bin/swl-telegram-bot.plist +21 -0
- package/bin/swl-telegram-bot.service +14 -0
- package/comandos/swl/.evolved.json +23 -0
- package/comandos/swl/actualizar.md +174 -0
- package/comandos/swl/adoptar-proyecto.md +207 -0
- package/comandos/swl/aprender.md +701 -0
- package/comandos/swl/auditar-deps.md +134 -0
- package/comandos/swl/autoresearch.md +170 -0
- package/comandos/swl/ayuda.md +224 -0
- package/comandos/swl/brainstorm.md +50 -0
- package/comandos/swl/checkpoint.md +330 -0
- package/comandos/swl/compactar.md +283 -0
- package/comandos/swl/configurar-ci.md +227 -0
- package/comandos/swl/contexto.md +112 -0
- package/comandos/swl/contribuir.md +233 -0
- package/comandos/swl/crear-skill.md +292 -0
- package/comandos/swl/cron.md +196 -0
- package/comandos/swl/dashboard.md +146 -0
- package/comandos/swl/discutir-fase.md +230 -0
- package/comandos/swl/ejecutar-fase.md +135 -0
- package/comandos/swl/evaluar-skill.md +487 -0
- package/comandos/swl/evolucion-estado.md +142 -0
- package/comandos/swl/evolucionar.md +259 -0
- package/comandos/swl/exportar-vault.md +189 -0
- package/comandos/swl/gateway.md +158 -0
- package/comandos/swl/inbox.md +116 -0
- package/comandos/swl/instalar.md +220 -0
- package/comandos/swl/instintos.md +86 -0
- package/comandos/swl/mapear-codebase.md +312 -0
- package/comandos/swl/mcp-status.md +175 -0
- package/comandos/swl/metricas.md +270 -0
- package/comandos/swl/modelo.md +102 -0
- package/comandos/swl/notificaciones.md +396 -0
- package/comandos/swl/nuevo-proyecto.md +154 -0
- package/comandos/swl/planear-fase.md +221 -0
- package/comandos/swl/plugins.md +256 -0
- package/comandos/swl/reflect-skills.md +125 -0
- package/comandos/swl/release.md +217 -0
- package/comandos/swl/revisar-impacto.md +206 -0
- package/comandos/swl/revisar.md +330 -0
- package/comandos/swl/salud.md +363 -0
- package/comandos/swl/sesiones.md +200 -0
- package/comandos/swl/skill-search.md +113 -0
- package/comandos/swl/verificar.md +585 -0
- package/comandos/swl/wiki.md +620 -0
- package/contextos/dev.md +32 -0
- package/contextos/research.md +30 -0
- package/contextos/review.md +31 -0
- package/habilidades/accesibilidad-a11y/SKILL.md +201 -0
- package/habilidades/accesibilidad-a11y/evals/evals.json +56 -0
- package/habilidades/accesibilidad-a11y/recursos/ejemplos-y-checklist-completo.md +441 -0
- package/habilidades/agent-browser/SKILL.md +218 -0
- package/habilidades/agentes-como-servicio/SKILL.md +218 -0
- package/habilidades/ai-runtime-security/SKILL.md +273 -0
- package/habilidades/angular-avanzado/SKILL.md +164 -0
- package/habilidades/angular-avanzado/recursos/ejemplos-avanzados.md +219 -0
- package/habilidades/angular-moderno/SKILL.md +186 -0
- package/habilidades/angular-moderno/evals/evals.json +45 -0
- package/habilidades/angular-moderno/recursos/ejemplos-avanzados.md +106 -0
- package/habilidades/api-rest-diseno/SKILL.md +191 -0
- package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
- package/habilidades/api-rest-diseno/recursos/referencia-api.md +140 -0
- package/habilidades/aprendizaje-continuo/SKILL.md +151 -0
- package/habilidades/aprendizaje-continuo/evals/evals.json +53 -0
- package/habilidades/aprendizaje-continuo/recursos/referencia-instintos.md +290 -0
- package/habilidades/async-python/SKILL.md +149 -0
- package/habilidades/async-python/evals/evals.json +47 -0
- package/habilidades/async-python/recursos/patrones-y-ejemplos-completos.md +292 -0
- package/habilidades/auth-patrones/.evolved.json +9 -0
- package/habilidades/auth-patrones/SKILL.md +413 -0
- package/habilidades/auth-patrones/recursos/implementaciones-completas.md +229 -0
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -0
- package/habilidades/auto-evolucion-protocolo/evals/evals.json +55 -0
- package/habilidades/auto-evolucion-protocolo/recursos/referencia-completa.md +145 -0
- package/habilidades/autoresearch/SKILL.md +268 -0
- package/habilidades/autoresearch/evals/evals.json +41 -0
- package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
- package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
- package/habilidades/azure-cloud/SKILL.md +308 -0
- package/habilidades/azure-cloud/recursos/aks.md +327 -0
- package/habilidades/backend-mcp-servidor/SKILL.md +270 -0
- package/habilidades/backend-production-resilience/SKILL.md +288 -0
- package/habilidades/brainstorming/SKILL.md +295 -0
- package/habilidades/brainstorming/recursos/componentes-html.md +247 -0
- package/habilidades/build-errors-cpp/SKILL.md +270 -0
- package/habilidades/build-errors-csharp/SKILL.md +265 -0
- package/habilidades/build-errors-go/SKILL.md +306 -0
- package/habilidades/build-errors-java/SKILL.md +278 -0
- package/habilidades/build-errors-kotlin/SKILL.md +303 -0
- package/habilidades/build-errors-nextjs/SKILL.md +312 -0
- package/habilidades/build-errors-php/SKILL.md +270 -0
- package/habilidades/build-errors-python/SKILL.md +292 -0
- package/habilidades/build-errors-rust/SKILL.md +284 -0
- package/habilidades/build-errors-swift/SKILL.md +272 -0
- package/habilidades/build-errors-typescript/SKILL.md +369 -0
- package/habilidades/checklist-calidad/SKILL.md +271 -0
- package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
- package/habilidades/checklist-seguridad/SKILL.md +285 -0
- package/habilidades/checkpoints-verificacion/SKILL.md +298 -0
- package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
- package/habilidades/ci-cd-pipelines/SKILL.md +157 -0
- package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
- package/habilidades/ci-cd-pipelines/recursos/pipelines-completos.md +487 -0
- package/habilidades/cloud-aws/SKILL.md +142 -0
- package/habilidades/cloud-aws/recursos/servicios-aws-referencia.md +321 -0
- package/habilidades/compactacion-contexto/SKILL.md +247 -0
- package/habilidades/contenedores-docker/SKILL.md +137 -0
- package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
- package/habilidades/contenedores-docker/recursos/ejemplos-y-configuraciones.md +327 -0
- package/habilidades/context-builder/SKILL.md +170 -0
- package/habilidades/control-profundidad/SKILL.md +128 -0
- package/habilidades/csharp-experto/SKILL.md +322 -0
- package/habilidades/csharp-patrones/SKILL.md +316 -0
- package/habilidades/csharp-testing/SKILL.md +286 -0
- package/habilidades/css-moderno/SKILL.md +166 -0
- package/habilidades/css-moderno/evals/evals.json +43 -0
- package/habilidades/css-moderno/recursos/ejemplos-y-patrones-completos.md +337 -0
- package/habilidades/datos-etl/SKILL.md +129 -0
- package/habilidades/datos-etl/recursos/implementaciones-completas.md +322 -0
- package/habilidades/dbml-experto/SKILL.md +339 -0
- package/habilidades/dbml-experto/evals/evals.json +56 -0
- package/habilidades/dependencias-auditoria/SKILL.md +320 -0
- package/habilidades/deprecacion-migracion/SKILL.md +169 -0
- package/habilidades/deprecacion-migracion/recursos/implementaciones-completas.md +220 -0
- package/habilidades/design-tokens/SKILL.md +158 -0
- package/habilidades/design-tokens/recursos/tokens-y-configuracion.md +363 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +309 -0
- package/habilidades/diagrama-arquitectura/SKILL.md +165 -0
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -0
- package/habilidades/discutir-fase/SKILL.md +188 -0
- package/habilidades/diseno-herramientas-agente/SKILL.md +199 -0
- package/habilidades/diseno-responsivo/SKILL.md +186 -0
- package/habilidades/diseno-responsivo/recursos/ejemplos-layouts.md +156 -0
- package/habilidades/django-experto/SKILL.md +205 -0
- package/habilidades/django-experto/recursos/async-django.md +390 -0
- package/habilidades/django-experto/recursos/drf-patrones.md +438 -0
- package/habilidades/django-experto/recursos/orm-avanzado.md +382 -0
- package/habilidades/django-experto/recursos/referencia-completa.md +188 -0
- package/habilidades/django-experto/recursos/testing-django.md +415 -0
- package/habilidades/doc-sync/SKILL.md +280 -0
- package/habilidades/drift-detection/SKILL.md +179 -0
- package/habilidades/ejecutar-fase/SKILL.md +468 -0
- package/habilidades/estilo-sin-ai-isms/SKILL.md +775 -0
- package/habilidades/estilo-sin-ai-isms/evals/evals.json +63 -0
- package/habilidades/estilo-sin-ai-isms/scripts/detectar_aiisms.py +500 -0
- package/habilidades/estructura-proyecto-claude/SKILL.md +215 -0
- package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
- package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +176 -0
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +289 -0
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
- package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
- package/habilidades/evaluacion-agentes/SKILL.md +314 -0
- package/habilidades/event-driven/SKILL.md +153 -0
- package/habilidades/event-driven/recursos/implementaciones-completas.md +423 -0
- package/habilidades/extraccion-documentos/SKILL.md +221 -0
- package/habilidades/extractor-de-aprendizajes/.evolved.json +9 -0
- package/habilidades/extractor-de-aprendizajes/SKILL.md +311 -0
- package/habilidades/extractor-de-aprendizajes/evals/evals.json +55 -0
- package/habilidades/fastapi-experto/SKILL.md +221 -0
- package/habilidades/fastapi-experto/recursos/async-patterns.md +438 -0
- package/habilidades/fastapi-experto/recursos/dependency-injection.md +330 -0
- package/habilidades/fastapi-experto/recursos/referencia-completa.md +79 -0
- package/habilidades/fastapi-experto/recursos/testing-httpx.md +420 -0
- package/habilidades/filament-admin/SKILL.md +290 -0
- package/habilidades/frontend-avanzado/SKILL.md +257 -0
- package/habilidades/frontend-avanzado/recursos/apis-nativas-ejemplos.md +341 -0
- package/habilidades/gcp-cloud/SKILL.md +260 -0
- package/habilidades/gcp-cloud/recursos/gke.md +234 -0
- package/habilidades/gcp-cloud/recursos/terraform-gcp.md +307 -0
- package/habilidades/generacion-mermaid/SKILL.md +229 -0
- package/habilidades/git-worktrees-paralelo/SKILL.md +270 -0
- package/habilidades/go-experto/SKILL.md +305 -0
- package/habilidades/go-patrones/SKILL.md +299 -0
- package/habilidades/go-testing/SKILL.md +291 -0
- package/habilidades/graphql-experto/SKILL.md +323 -0
- package/habilidades/guardrail-semantico/SKILL.md +282 -0
- package/habilidades/harness-claude-code/SKILL.md +299 -0
- package/habilidades/iam-secretos/SKILL.md +265 -0
- package/habilidades/iam-secretos/recursos/implementaciones-completas.md +356 -0
- package/habilidades/infra-github-actions/SKILL.md +166 -0
- package/habilidades/instalar-sistema/.evolved.json +9 -0
- package/habilidades/instalar-sistema/SKILL.md +221 -0
- package/habilidades/java-experto/SKILL.md +290 -0
- package/habilidades/java-patrones/SKILL.md +275 -0
- package/habilidades/java-testing/SKILL.md +288 -0
- package/habilidades/kotlin-compose/SKILL.md +278 -0
- package/habilidades/kotlin-compose/recursos/animaciones-performance.md +93 -0
- package/habilidades/kotlin-experto/SKILL.md +318 -0
- package/habilidades/kotlin-testing/SKILL.md +267 -0
- package/habilidades/kotlin-testing/recursos/testing-avanzado.md +74 -0
- package/habilidades/kubernetes-orquestacion/SKILL.md +152 -0
- package/habilidades/kubernetes-orquestacion/recursos/manifiestos-completos.md +452 -0
- package/habilidades/langchain-langraph/SKILL.md +386 -0
- package/habilidades/langchain-langraph/recursos/evaluacion-rag.md +321 -0
- package/habilidades/langchain-langraph/recursos/rag-maturity-model.md +225 -0
- package/habilidades/langchain-langraph/recursos/vectorstores.md +306 -0
- package/habilidades/legacy-code-rescue/SKILL.md +267 -0
- package/habilidades/likec4-experto/SKILL.md +412 -0
- package/habilidades/likec4-experto/evals/evals.json +69 -0
- package/habilidades/manejo-errores/.evolved.json +9 -0
- package/habilidades/manejo-errores/SKILL.md +407 -0
- package/habilidades/manejo-errores/recursos/implementaciones-completas.md +248 -0
- package/habilidades/mapear-codebase/SKILL.md +275 -0
- package/habilidades/memoria-busqueda/SKILL.md +194 -0
- package/habilidades/memoria-busqueda/evals/evals.json +44 -0
- package/habilidades/meta-skills-estandar/SKILL.md +298 -0
- package/habilidades/meta-skills-estandar/recursos/anti-patrones-y-leyes.md +205 -0
- package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +107 -0
- package/habilidades/meta-skills-estandar/recursos/idiomas-framework.md +60 -0
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -0
- package/habilidades/microservicios/SKILL.md +155 -0
- package/habilidades/microservicios/recursos/patrones-y-ejemplos-completos.md +325 -0
- package/habilidades/mobile-flutter/SKILL.md +199 -0
- package/habilidades/mobile-flutter/recursos/ejemplos-completos.md +319 -0
- package/habilidades/mobile-react-native/SKILL.md +176 -0
- package/habilidades/mobile-react-native/recursos/ejemplos-completos.md +216 -0
- package/habilidades/mongodb-experto/SKILL.md +302 -0
- package/habilidades/monitoring-alertas/SKILL.md +201 -0
- package/habilidades/monitoring-alertas/recursos/instrumentacion-y-alertas.md +301 -0
- package/habilidades/nestjs-experto/SKILL.md +307 -0
- package/habilidades/nestjs-experto/recursos/guards-interceptors.md +339 -0
- package/habilidades/nestjs-experto/recursos/modulos-di.md +287 -0
- package/habilidades/nestjs-experto/recursos/testing-nestjs.md +354 -0
- package/habilidades/nextjs-experto/SKILL.md +335 -0
- package/habilidades/nextjs-patrones/SKILL.md +303 -0
- package/habilidades/nextjs-testing/SKILL.md +331 -0
- package/habilidades/node-experto/.evolved.json +9 -0
- package/habilidades/node-experto/SKILL.md +266 -0
- package/habilidades/node-experto/recursos/patrones-completos.md +283 -0
- package/habilidades/notificaciones-multicanal/SKILL.md +159 -0
- package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
- package/habilidades/notificaciones-multicanal/recursos/configuracion-y-templates.md +303 -0
- package/habilidades/nuevo-proyecto/SKILL.md +204 -0
- package/habilidades/orquestacion-async/SKILL.md +303 -0
- package/habilidades/paid-media-tracking/SKILL.md +269 -0
- package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +220 -0
- package/habilidades/paid-media-tracking/recursos/google-ads-api.md +215 -0
- package/habilidades/patrones-python/SKILL.md +228 -0
- package/habilidades/patrones-python/evals/evals.json +56 -0
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -0
- package/habilidades/patrones-python/recursos/referencia-completa.md +202 -0
- package/habilidades/perfil-usuario/SKILL.md +200 -0
- package/habilidades/perfil-usuario/evals/evals.json +55 -0
- package/habilidades/performance-baseline/SKILL.md +297 -0
- package/habilidades/php-experto/SKILL.md +291 -0
- package/habilidades/php-patrones/SKILL.md +306 -0
- package/habilidades/php-testing/SKILL.md +280 -0
- package/habilidades/planear-fase/SKILL.md +269 -0
- package/habilidades/postgresql-experto/SKILL.md +151 -0
- package/habilidades/postgresql-experto/evals/evals.json +53 -0
- package/habilidades/postgresql-experto/recursos/referencia-completa.md +215 -0
- package/habilidades/prevencion-racionalizacion/SKILL.md +175 -0
- package/habilidades/prevencion-sobreingenieria/SKILL.md +323 -0
- package/habilidades/privacy-memoria/SKILL.md +141 -0
- package/habilidades/privacy-memoria/evals/evals.json +43 -0
- package/habilidades/prompt-engineering/SKILL.md +518 -0
- package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +467 -0
- package/habilidades/rag-arquitectura/SKILL.md +338 -0
- package/habilidades/rails-experto/SKILL.md +237 -0
- package/habilidades/rails-experto/recursos/active-record.md +260 -0
- package/habilidades/rails-experto/recursos/hotwire-turbo.md +293 -0
- package/habilidades/rails-experto/recursos/testing-rspec.md +362 -0
- package/habilidades/react-experto/SKILL.md +209 -0
- package/habilidades/react-experto/evals/evals.json +55 -0
- package/habilidades/react-experto/recursos/patrones-y-ejemplos-completos.md +240 -0
- package/habilidades/react-optimizacion/SKILL.md +174 -0
- package/habilidades/react-optimizacion/recursos/patrones-avanzados.md +138 -0
- package/habilidades/redis-experto/SKILL.md +305 -0
- package/habilidades/release-semver/.evolved.json +9 -0
- package/habilidades/release-semver/SKILL.md +248 -0
- package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
- package/habilidades/rust-experto/SKILL.md +400 -0
- package/habilidades/rust-patrones/SKILL.md +296 -0
- package/habilidades/rust-testing/SKILL.md +311 -0
- package/habilidades/seguridad-skills-ia/SKILL.md +262 -0
- package/habilidades/sql-optimizacion/SKILL.md +200 -0
- package/habilidades/sql-optimizacion/evals/evals.json +54 -0
- package/habilidades/sql-optimizacion/recursos/patrones-sql-avanzados.md +131 -0
- package/habilidades/sre-patrones/SKILL.md +333 -0
- package/habilidades/sre-patrones/recursos/chaos-engineering.md +241 -0
- package/habilidades/sre-patrones/recursos/oncall-design.md +236 -0
- package/habilidades/stripe-pagos/SKILL.md +550 -0
- package/habilidades/stripe-pagos/recursos/errores-reintentos.md +390 -0
- package/habilidades/stripe-pagos/recursos/stripe-connect.md +290 -0
- package/habilidades/structured-outputs/SKILL.md +343 -0
- package/habilidades/swift-experto/SKILL.md +320 -0
- package/habilidades/swift-experto/recursos/keychain-y-wrappers.md +110 -0
- package/habilidades/swift-patrones/SKILL.md +313 -0
- package/habilidades/swift-patrones/recursos/tca-ejemplo-completo.md +113 -0
- package/habilidades/swift-testing/SKILL.md +254 -0
- package/habilidades/swift-testing/recursos/xcuitest-planes.md +143 -0
- package/habilidades/swl-dashboard/SKILL.md +370 -0
- package/habilidades/swl-markitdown/SKILL.md +285 -0
- package/habilidades/swl-markitdown/evals/evals.json +52 -0
- package/habilidades/swl-revisar-impacto/SKILL.md +233 -0
- package/habilidades/tailwind-experto/SKILL.md +240 -0
- package/habilidades/tailwind-experto/recursos/referencia-completa.md +184 -0
- package/habilidades/tdd-workflow/SKILL.md +293 -0
- package/habilidades/terraform-experto/SKILL.md +321 -0
- package/habilidades/testing-python/SKILL.md +340 -0
- package/habilidades/testing-python/recursos/ejemplos-completos.md +167 -0
- package/habilidades/threat-model-lite/SKILL.md +246 -0
- package/habilidades/tracing-processor/SKILL.md +212 -0
- package/habilidades/tracking-measurement/SKILL.md +239 -0
- package/habilidades/tracking-measurement/recursos/consent-mode.md +231 -0
- package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +216 -0
- package/habilidades/tracking-measurement/recursos/meta-capi.md +262 -0
- package/habilidades/typescript-avanzado/SKILL.md +144 -0
- package/habilidades/typescript-avanzado/evals/evals.json +55 -0
- package/habilidades/typescript-avanzado/recursos/patrones-y-ejemplos-completos.md +298 -0
- package/habilidades/typescript-diagnosticos/SKILL.md +513 -0
- package/habilidades/ux-diseno/SKILL.md +116 -0
- package/habilidades/ux-diseno/evals/evals.json +43 -0
- package/habilidades/ux-diseno/recursos/patrones-ux-referencia.md +214 -0
- package/habilidades/validacion-ci-sistema/SKILL.md +136 -0
- package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +369 -0
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
- package/habilidades/verificacion-evidencia/SKILL.md +160 -0
- package/habilidades/verificar-trabajo/SKILL.md +303 -0
- package/habilidades/verificar-trabajo/recursos/plantilla-verificacion.md +60 -0
- package/habilidades/wiki-conocimiento/SKILL.md +276 -0
- package/habilidades/wireframes-flujos/SKILL.md +212 -0
- package/habilidades/wireframes-flujos/recursos/referencia-completa.md +192 -0
- package/habilidades/workflow-claude-code/SKILL.md +260 -0
- package/habilidades/workflow-claude-code/recursos/referencia-completa.md +109 -0
- package/hooks/_run-hook.sh +57 -0
- package/hooks/actualizar-perfil-usuario.js +364 -0
- package/hooks/agente-lifecycle.js +71 -0
- package/hooks/aiisms-detector.js +173 -0
- package/hooks/audit-trail.js +204 -0
- package/hooks/auto-background.js +97 -0
- package/hooks/auto-consolidacion.js +178 -0
- package/hooks/auto-evolucion.js +666 -0
- package/hooks/auto-restaurar-settings.js +360 -0
- package/hooks/calidad-pre-commit.js +929 -0
- package/hooks/calidad-typescript.js +511 -0
- package/hooks/captura-feedback-usuario.js +148 -0
- package/hooks/check-update.js +211 -0
- package/hooks/clasificador-mensajes.js +271 -0
- package/hooks/degradacion-instintos.js +272 -0
- package/hooks/escaneo-secretos.js +389 -0
- package/hooks/extraccion-aprendizajes.js +763 -0
- package/hooks/grafo-contexto.js +129 -0
- package/hooks/graph-update.js +67 -0
- package/hooks/guardrail-modelo.js +247 -0
- package/hooks/inbox-aviso.js +75 -0
- package/hooks/inyeccion-contexto.js +246 -0
- package/hooks/lib/abort-registry.js +214 -0
- package/hooks/lib/agent-backend.js +210 -0
- package/hooks/lib/agent-comms.js +263 -0
- package/hooks/lib/agent-issue-codes.js +284 -0
- package/hooks/lib/agent-matcher.js +189 -0
- package/hooks/lib/async-hook-registry.js +252 -0
- package/hooks/lib/atomic-write.js +130 -0
- package/hooks/lib/auto-consolidator.js +335 -0
- package/hooks/lib/canary-skills.js +187 -0
- package/hooks/lib/consolidation-lock.js +291 -0
- package/hooks/lib/context-builder.js +430 -0
- package/hooks/lib/context-compressor.js +657 -0
- package/hooks/lib/convergence-detector.js +105 -0
- package/hooks/lib/delegation-tracker.js +198 -0
- package/hooks/lib/detectar-package-manager.js +423 -0
- package/hooks/lib/edit-accumulator.js +171 -0
- package/hooks/lib/error-classifier.js +308 -0
- package/hooks/lib/event-bus.js +112 -0
- package/hooks/lib/evolution-tracker.js +442 -0
- package/hooks/lib/execution-state.js +316 -0
- package/hooks/lib/fingerprint-id.js +135 -0
- package/hooks/lib/gateway-notify.js +116 -0
- package/hooks/lib/graph-security.js +75 -0
- package/hooks/lib/guardrail-metrics.js +202 -0
- package/hooks/lib/hook-circuit-breaker.js +206 -0
- package/hooks/lib/loop-detector.js +267 -0
- package/hooks/lib/mcp-health.js +184 -0
- package/hooks/lib/mcp-pool.js +436 -0
- package/hooks/lib/memory-search.js +506 -0
- package/hooks/lib/merkle-audit.js +96 -0
- package/hooks/lib/model-router.js +222 -0
- package/hooks/lib/normalize-error.js +324 -0
- package/hooks/lib/normalize-input.js +65 -0
- package/hooks/lib/nudge-tracker.js +306 -0
- package/hooks/lib/otlp-exporter.js +365 -0
- package/hooks/lib/performance-marks.js +239 -0
- package/hooks/lib/privacy-filter.js +128 -0
- package/hooks/lib/prompt-injection-scanner.js +209 -0
- package/hooks/lib/provenance-tracker.js +183 -0
- package/hooks/lib/rate-limit-tracker.js +253 -0
- package/hooks/lib/reflect-classifier.js +164 -0
- package/hooks/lib/resource-quota.js +122 -0
- package/hooks/lib/retry-jitter.js +165 -0
- package/hooks/lib/risk-engine.js +368 -0
- package/hooks/lib/run-log.js +408 -0
- package/hooks/lib/session-fts.js +379 -0
- package/hooks/lib/session-store.js +293 -0
- package/hooks/lib/singleton-guard.js +159 -0
- package/hooks/lib/skill-auditor.js +588 -0
- package/hooks/lib/sync-status.js +228 -0
- package/hooks/lib/taint-tracker.js +107 -0
- package/hooks/lib/task-service.js +295 -0
- package/hooks/lib/tech-skills-map.js +146 -0
- package/hooks/lib/telegram-cliente.js +159 -0
- package/hooks/lib/telegram-config.js +170 -0
- package/hooks/lib/token-budget.js +156 -0
- package/hooks/lib/token-estimator.js +420 -0
- package/hooks/lib/toon-compressor.js +245 -0
- package/hooks/lib/usage-model.js +183 -0
- package/hooks/lib/variable-resolver.js +230 -0
- package/hooks/linea-estado.js +324 -0
- package/hooks/metricas-evolucion.js +209 -0
- package/hooks/monitor-contexto.js +325 -0
- package/hooks/notificacion-sesion-stop.js +198 -0
- package/hooks/notificacion-telegram-notification.js +4 -0
- package/hooks/notificacion-telegram-subagent.js +4 -0
- package/hooks/notificacion-telegram.js +267 -0
- package/hooks/preservar-estado-pre-compact.js +150 -0
- package/hooks/proteccion-rutas.js +366 -0
- package/hooks/registro-turnos.js +209 -0
- package/hooks/resumen-sesion.js +249 -0
- package/hooks/risk-scoring.js +323 -0
- package/hooks/rotar-audit-auto.js +122 -0
- package/hooks/sugerir-regenerar-inventario.js +170 -0
- package/hooks/telemetria-agentes.js +167 -0
- package/hooks/tracking-costos.js +688 -0
- package/instintos/global.yaml +8 -0
- package/instintos/perfil-usuario.yaml +53 -0
- package/instintos/prompt-appendices.yaml +57 -0
- package/instintos/proyecto.yaml +372 -0
- package/manifiestos/gateway-config.json +77 -0
- package/manifiestos/handoff-context.json +223 -0
- package/manifiestos/hook-profiles.json +44 -0
- package/manifiestos/hooks-config.json +360 -0
- package/manifiestos/modulos.json +1173 -0
- package/manifiestos/perfiles.json +404 -0
- package/package.json +86 -0
- package/plantillas/ESTADO.md +109 -0
- package/plantillas/HOJA-RUTA.md +143 -0
- package/plantillas/PROYECTO.md +122 -0
- package/plantillas/REQUISITOS.md +132 -0
- package/plantillas/auditor-veto-template.md +105 -0
- package/plantillas/github-workflows/README.md +47 -0
- package/plantillas/github-workflows/release-please.yml +44 -0
- package/plantillas/github-workflows/swl-ci.yml +107 -0
- package/plantillas/github-workflows/swl-security.yml +51 -0
- package/plantillas/mcp-mineru.json +13 -0
- package/plantillas/research/ARQUITECTURA.md +220 -0
- package/plantillas/research/FUNCIONALIDADES.md +175 -0
- package/plantillas/research/RESUMEN.md +165 -0
- package/plantillas/research/STACK.md +233 -0
- package/plantillas/research/TRAMPAS.md +299 -0
- package/plantillas/skill-evals-template.json +44 -0
- package/plugin.json +343 -0
- package/reglas/accesibilidad.md +269 -0
- package/reglas/api-diseno.md +400 -0
- package/reglas/arquitectura.md +352 -0
- package/reglas/brevedad-output.md +124 -0
- package/reglas/cloud-infra.md +247 -0
- package/reglas/docs.md +245 -0
- package/reglas/estilo-codigo.md +201 -0
- package/reglas/git-workflow.md +245 -0
- package/reglas/gobernanza.md +271 -0
- package/reglas/harness-claude-code.md +213 -0
- package/reglas/hooks.md +186 -0
- package/reglas/lenguajes/csharp/estilo-codigo.md +231 -0
- package/reglas/lenguajes/csharp/hooks.md +281 -0
- package/reglas/lenguajes/csharp/patrones.md +226 -0
- package/reglas/lenguajes/csharp/seguridad.md +258 -0
- package/reglas/lenguajes/csharp/testing.md +176 -0
- package/reglas/lenguajes/go/estilo-codigo.md +195 -0
- package/reglas/lenguajes/go/hooks.md +249 -0
- package/reglas/lenguajes/go/patrones.md +249 -0
- package/reglas/lenguajes/go/seguridad.md +225 -0
- package/reglas/lenguajes/go/testing.md +272 -0
- package/reglas/lenguajes/java/estilo-codigo.md +217 -0
- package/reglas/lenguajes/java/hooks.md +251 -0
- package/reglas/lenguajes/java/patrones.md +226 -0
- package/reglas/lenguajes/java/seguridad.md +233 -0
- package/reglas/lenguajes/java/testing.md +238 -0
- package/reglas/lenguajes/kotlin/estilo-codigo.md +208 -0
- package/reglas/lenguajes/kotlin/hooks.md +245 -0
- package/reglas/lenguajes/kotlin/patrones.md +201 -0
- package/reglas/lenguajes/kotlin/seguridad.md +202 -0
- package/reglas/lenguajes/kotlin/testing.md +236 -0
- package/reglas/lenguajes/nextjs/estilo-codigo.md +175 -0
- package/reglas/lenguajes/nextjs/hooks.md +186 -0
- package/reglas/lenguajes/nextjs/patrones.md +225 -0
- package/reglas/lenguajes/nextjs/seguridad.md +216 -0
- package/reglas/lenguajes/nextjs/testing.md +193 -0
- package/reglas/lenguajes/php/estilo-codigo.md +228 -0
- package/reglas/lenguajes/php/hooks.md +165 -0
- package/reglas/lenguajes/php/patrones.md +233 -0
- package/reglas/lenguajes/php/seguridad.md +186 -0
- package/reglas/lenguajes/php/testing.md +205 -0
- package/reglas/lenguajes/rust/estilo-codigo.md +207 -0
- package/reglas/lenguajes/rust/hooks.md +240 -0
- package/reglas/lenguajes/rust/patrones.md +250 -0
- package/reglas/lenguajes/rust/seguridad.md +221 -0
- package/reglas/lenguajes/rust/testing.md +194 -0
- package/reglas/lenguajes/swift/estilo-codigo.md +238 -0
- package/reglas/lenguajes/swift/hooks.md +257 -0
- package/reglas/lenguajes/swift/patrones.md +235 -0
- package/reglas/lenguajes/swift/seguridad.md +248 -0
- package/reglas/lenguajes/swift/testing.md +242 -0
- package/reglas/markitdown.md +60 -0
- package/reglas/memoria-consolidada.md +209 -0
- package/reglas/patrones.md +225 -0
- package/reglas/performance.md +195 -0
- package/reglas/pruebas.md +159 -0
- package/reglas/seguridad-agentes.md +351 -0
- package/reglas/seguridad.md +151 -0
- package/reglas/skills-estandar.md +373 -0
- package/reglas/testing.md +193 -0
- package/schemas/agent-contract.json +176 -0
- package/schemas/agent-frontmatter.schema.json +149 -0
- package/schemas/agent-message.schema.json +53 -0
- package/schemas/agent-output-implementacion.schema.json +85 -0
- package/schemas/agent-output-planificacion.schema.json +113 -0
- package/schemas/agent-output-review.schema.json +78 -0
- package/schemas/diary-entry.schema.json +80 -0
- package/schemas/hook-profiles.schema.json +39 -0
- package/schemas/hooks-config.schema.json +74 -0
- package/schemas/instinct.schema.json +115 -0
- package/schemas/modulos.schema.json +29 -0
- package/schemas/perfiles.schema.json +28 -0
- package/schemas/plugin.schema.json +64 -0
- package/schemas/skill-evals.schema.json +95 -0
- package/schemas/skill-frontmatter.schema.json +170 -0
- package/scripts/actualizar.js +145 -0
- package/scripts/audit-skills.sh +78 -0
- package/scripts/auditar-agentes-gaps.js +149 -0
- package/scripts/auditar-cobertura-frameworks.js +241 -0
- package/scripts/auditar-skills-gaps.js +206 -0
- package/scripts/bootstrap-instintos.js +259 -0
- package/scripts/check-update.js +109 -0
- package/scripts/comandos/agents.js +105 -0
- package/scripts/comandos/info.js +108 -0
- package/scripts/comandos/install-asistido.js +186 -0
- package/scripts/comandos/skills.js +211 -0
- package/scripts/configurar-branch-protection.js +418 -0
- package/scripts/daemon-swl.py +388 -0
- package/scripts/desinstalar.js +130 -0
- package/scripts/doctor.js +559 -0
- package/scripts/field-report.js +199 -0
- package/scripts/generar-inventario.js +317 -0
- package/scripts/inbox-tmux-inject.js +161 -0
- package/scripts/inferir-herramientas-permitidas.js +586 -0
- package/scripts/inicializar.js +133 -0
- package/scripts/instalador.js +1031 -0
- package/scripts/instalar-git-hook.js +122 -0
- package/scripts/lib/agp-frontmatter.js +222 -0
- package/scripts/lib/append-con-marcadores.js +199 -0
- package/scripts/lib/artefactos-python.js +43 -0
- package/scripts/lib/audit-query.js +221 -0
- package/scripts/lib/autostart-linux.js +347 -0
- package/scripts/lib/autostart-macos.js +360 -0
- package/scripts/lib/autostart-windows.js +307 -0
- package/scripts/lib/budget-enforcer.js +252 -0
- package/scripts/lib/claude-sessions.js +285 -0
- package/scripts/lib/configurar-ci.js +380 -0
- package/scripts/lib/console-span-exporter.js +92 -0
- package/scripts/lib/contadores-inventario.js +217 -0
- package/scripts/lib/dashboard-widgets.js +290 -0
- package/scripts/lib/detectar-runtime.js +279 -0
- package/scripts/lib/detectar-stack.js +187 -0
- package/scripts/lib/diary-entry.js +234 -0
- package/scripts/lib/drift-detector.js +545 -0
- package/scripts/lib/estado.js +124 -0
- package/scripts/lib/gestor-componentes.js +243 -0
- package/scripts/lib/gitignore-manifest.js +305 -0
- package/scripts/lib/graph-analyze.py +556 -0
- package/scripts/lib/graph-builder.py +485 -0
- package/scripts/lib/graph-cluster.py +259 -0
- package/scripts/lib/health-row.js +168 -0
- package/scripts/lib/hooks-settings.js +789 -0
- package/scripts/lib/manifiestos.js +138 -0
- package/scripts/lib/mc-client.js +137 -0
- package/scripts/lib/notificaciones-telegram.js +1107 -0
- package/scripts/lib/npm-version.js +261 -0
- package/scripts/lib/paquetes-conocidos.js +50 -0
- package/scripts/lib/preservar-usuario.js +586 -0
- package/scripts/lib/prompt-builder.js +264 -0
- package/scripts/lib/resolver-externo.js +332 -0
- package/scripts/lib/schedule-parser.js +305 -0
- package/scripts/lib/scoring-instintos.js +240 -0
- package/scripts/lib/seguridad.js +160 -0
- package/scripts/lib/selector-interactivo.js +152 -0
- package/scripts/lib/semantic-search.js +242 -0
- package/scripts/lib/skill-discovery.js +234 -0
- package/scripts/lib/skill-metrics.js +246 -0
- package/scripts/lib/skill-normalizer.js +112 -0
- package/scripts/lib/skills-hub.js +340 -0
- package/scripts/lib/span-schema.js +134 -0
- package/scripts/lib/tool-cost-analyzer.js +255 -0
- package/scripts/lib/tracing-processor-interface.js +286 -0
- package/scripts/lib/transformadores/base.js +80 -0
- package/scripts/lib/transformadores/claude.js +124 -0
- package/scripts/lib/transformadores/codex.js +115 -0
- package/scripts/lib/transformadores/copilot.js +106 -0
- package/scripts/lib/transformadores/gemini.js +74 -0
- package/scripts/lib/transformadores/index.js +35 -0
- package/scripts/lib/transformadores/opencode.js +75 -0
- package/scripts/lib/ui.js +259 -0
- package/scripts/limpiar-artefactos-python.js +131 -0
- package/scripts/mcp-orchestrator.py +386 -0
- package/scripts/mcp-pool-manager.py +352 -0
- package/scripts/mcp-telemetry.py +378 -0
- package/scripts/poblar-evolvable.js +226 -0
- package/scripts/publicar.js +287 -0
- package/scripts/reflect-skills.js +403 -0
- package/scripts/rotar-audit-logs.js +185 -0
- package/scripts/run-skill-evals.js +242 -0
- package/scripts/smoke-test.js +374 -0
- package/scripts/token-analysis.py +471 -0
- package/scripts/validar-manifest.js +195 -0
- package/scripts/validar-memoria.js +321 -0
- package/scripts/validar-tests-aislamiento.js +184 -0
- package/scripts/validar-tokens-test.js +208 -0
- package/scripts/validar.js +147 -0
- package/scripts/validate-markdown.py +339 -0
- package/scripts/validate-skills.py +385 -0
- package/scripts/vendor/claude-usage/README.md +116 -0
- package/scripts/vendor/claude-usage/cli.py +334 -0
- package/scripts/vendor/claude-usage/dashboard.py +795 -0
- package/scripts/vendor/claude-usage/scanner.py +467 -0
- package/scripts/vendor/markitdown/cli.py +194 -0
- package/scripts/verificar-evolucion.js +289 -0
- package/scripts/verificar-release.js +494 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flujo reutilizable de configuración de notificaciones Telegram.
|
|
5
|
+
*
|
|
6
|
+
* Usado por:
|
|
7
|
+
* - scripts/instalador.js (paso 9 opt-in)
|
|
8
|
+
* - /swl:notificaciones init (Slice 005)
|
|
9
|
+
*
|
|
10
|
+
* SEGURIDAD:
|
|
11
|
+
* - El token NUNCA se loggea. Solo se muestra longitud y primeros 3 chars.
|
|
12
|
+
* - El .env se escribe en ~/.claude/notifications/ (fuera del repo).
|
|
13
|
+
* - Permisos 0o600 en POSIX; en Windows la ACL del sistema ya protege
|
|
14
|
+
* %USERPROFILE%\.claude\.
|
|
15
|
+
* - Validación post-escritura con JSON.parse + rollback si falla.
|
|
16
|
+
* - Backup obligatorio de settings.json antes de cada merge.
|
|
17
|
+
*
|
|
18
|
+
* ADR-0009: esta función es la ÚNICA excepción a la regla
|
|
19
|
+
* "SWL solo registra hooks a nivel proyecto" (hooks-settings.js).
|
|
20
|
+
* Aplica exclusivamente bajo opt-in explícito del usuario.
|
|
21
|
+
*
|
|
22
|
+
* @module scripts/lib/notificaciones-telegram
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
const os = require('node:os');
|
|
28
|
+
const readline = require('node:readline');
|
|
29
|
+
const { execFileSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
const { atomicWriteSync, atomicWriteJSON } = require('../../hooks/lib/atomic-write');
|
|
32
|
+
// IMPORTANTE: usar require sin destructuring para que los mocks de tests funcionen.
|
|
33
|
+
// El destructuring captura la referencia local original y los mocks posteriores
|
|
34
|
+
// no tienen efecto. Con el módulo completo, hooksSettings.mergeHooksGlobales(...)
|
|
35
|
+
// siempre resuelve la propiedad en tiempo de ejecución. (Fix Slice 015-A)
|
|
36
|
+
const hooksSettings = require('./hooks-settings');
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Rutas canónicas (NUNCA dentro del repo)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const DIR_NOTIFICACIONES = path.join(os.homedir(), '.claude', 'notifications');
|
|
43
|
+
const RUTA_PID_DAEMON = path.join(DIR_NOTIFICACIONES, 'bot.pid');
|
|
44
|
+
const RUTA_BIN_DAEMON = path.resolve(__dirname, '..', '..', 'bin', 'swl-telegram-bot.js');
|
|
45
|
+
const RUTA_ENV = path.join(DIR_NOTIFICACIONES, '.env');
|
|
46
|
+
const RUTA_BASELINE = path.join(DIR_NOTIFICACIONES, 'baseline-hooks.json');
|
|
47
|
+
const RUTA_SETTINGS_GLOBAL = path.join(os.homedir(), '.claude', 'settings.json');
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Directorio global de hooks (~/.claude/hooks/)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Directorio destino donde se copian los hooks para ejecución global. */
|
|
54
|
+
const HOOKS_GLOBAL_DIR = path.join(os.homedir(), '.claude', 'hooks');
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lista de archivos a copiar desde <repo>/hooks/ a ~/.claude/hooks/.
|
|
58
|
+
* Se preserva la estructura lib/ para que los requires relativos funcionen.
|
|
59
|
+
*
|
|
60
|
+
* Orden importa para resolución de dependencias: lib/ primero, hooks raíz después.
|
|
61
|
+
*/
|
|
62
|
+
const HOOKS_A_COPIAR = [
|
|
63
|
+
'lib/atomic-write.js', // dependencia de auto-restaurar y notificacion
|
|
64
|
+
'lib/telegram-cliente.js',
|
|
65
|
+
'lib/telegram-config.js',
|
|
66
|
+
'notificacion-telegram.js',
|
|
67
|
+
'notificacion-telegram-notification.js',
|
|
68
|
+
'notificacion-telegram-subagent.js',
|
|
69
|
+
'auto-restaurar-settings.js',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Hooks SWL que se registran en el settings global bajo opt-in
|
|
73
|
+
const HOOKS_SWL_NOTIFICACIONES = [
|
|
74
|
+
{
|
|
75
|
+
evento: 'Stop',
|
|
76
|
+
matcher: '',
|
|
77
|
+
archivo: 'notificacion-telegram.js',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
evento: 'Notification',
|
|
81
|
+
matcher: '',
|
|
82
|
+
archivo: 'notificacion-telegram.js',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
evento: 'SubagentStop',
|
|
86
|
+
matcher: '',
|
|
87
|
+
archivo: 'notificacion-telegram.js',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
evento: 'SessionStart',
|
|
91
|
+
matcher: '',
|
|
92
|
+
archivo: 'auto-restaurar-settings.js',
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Copia de hooks a ~/.claude/hooks/ (REM-A, REM-E)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Copia los archivos de hook desde el repo swl-ses a ~/.claude/hooks/.
|
|
102
|
+
* Preserva la estructura lib/ para que los requires relativos funcionen.
|
|
103
|
+
*
|
|
104
|
+
* Verifica que cada archivo origen existe ANTES de copiar (REM-E).
|
|
105
|
+
* Si algún origen falta, lanza con mensaje claro que indica la ruta esperada.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} [hooksOrigenOverride] - Para tests: sobreescribir el directorio origen.
|
|
108
|
+
* @param {string} [hooksGlobalDirOverride] - Para tests: sobreescribir el directorio destino.
|
|
109
|
+
* @returns {{ directorio: string, archivos: string[] }}
|
|
110
|
+
* @throws {Error} Si algún archivo origen no existe o la copia falla.
|
|
111
|
+
*/
|
|
112
|
+
function _copiarHooksGlobales(hooksOrigenOverride, hooksGlobalDirOverride) {
|
|
113
|
+
// __dirname apunta a <repo-swl>/scripts/lib/ → subir 2 niveles → <repo-swl>/hooks/
|
|
114
|
+
const hooksOrigen = hooksOrigenOverride || path.resolve(__dirname, '..', '..', 'hooks');
|
|
115
|
+
const hooksDestino = hooksGlobalDirOverride || HOOKS_GLOBAL_DIR;
|
|
116
|
+
|
|
117
|
+
// Crear directorios destino si no existen
|
|
118
|
+
if (!fs.existsSync(hooksDestino)) {
|
|
119
|
+
fs.mkdirSync(hooksDestino, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
const libDestino = path.join(hooksDestino, 'lib');
|
|
122
|
+
if (!fs.existsSync(libDestino)) {
|
|
123
|
+
fs.mkdirSync(libDestino, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const copiados = [];
|
|
127
|
+
for (const rel of HOOKS_A_COPIAR) {
|
|
128
|
+
const origen = path.join(hooksOrigen, rel);
|
|
129
|
+
const destino = path.join(hooksDestino, rel);
|
|
130
|
+
|
|
131
|
+
// REM-E: verificar que el archivo origen existe
|
|
132
|
+
if (!fs.existsSync(origen)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Hook origen no encontrado: ${origen}. ` +
|
|
135
|
+
`Verifica que swl-ses está instalado correctamente en: ${hooksOrigen}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.copyFileSync(origen, destino);
|
|
140
|
+
|
|
141
|
+
// REM-E: verificar integridad post-copia (existe + tamaño > 0)
|
|
142
|
+
const stats = fs.statSync(destino);
|
|
143
|
+
if (stats.size === 0) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Hook copiado con tamaño 0: ${destino}. ` +
|
|
146
|
+
`El archivo origen puede estar corrupto: ${origen}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
copiados.push(rel);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Loggear en init.log solo paths y nombres — NUNCA secrets
|
|
154
|
+
const logPath = path.join(DIR_NOTIFICACIONES, 'init.log');
|
|
155
|
+
const logLinea = `[${new Date().toISOString()}] hooks copiados a ${hooksDestino}: ${copiados.join(', ')}\n`;
|
|
156
|
+
try { fs.appendFileSync(logPath, logLinea, 'utf8'); } catch (_) {}
|
|
157
|
+
|
|
158
|
+
return { directorio: hooksDestino, archivos: copiados };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Helpers internos
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Verifica que una ruta está dentro del directorio ~/.claude/notifications/
|
|
167
|
+
* y nunca dentro de un repositorio git.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} ruta - Ruta absoluta a verificar.
|
|
170
|
+
* @returns {boolean} true si la ruta es segura.
|
|
171
|
+
*/
|
|
172
|
+
function _esRutaSegura(ruta) {
|
|
173
|
+
const rutaNorm = path.resolve(ruta);
|
|
174
|
+
const dirNorm = path.resolve(DIR_NOTIFICACIONES);
|
|
175
|
+
return rutaNorm.startsWith(dirNorm);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Oculta un token para logging: muestra primeros 3 chars + ***.
|
|
180
|
+
* NUNCA loggear el token completo.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} token
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
function _ocultarToken(token) {
|
|
186
|
+
if (!token || token.length < 4) return '***';
|
|
187
|
+
return token.slice(0, 3) + '*** (long: ' + token.length + ')';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Genera el contenido del archivo .env con las credenciales.
|
|
192
|
+
* Preserva caracteres especiales (`:`, `=`, `_`) sin escape adicional.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} token
|
|
195
|
+
* @param {string} chatId
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function _generarContenidoEnv(token, chatId) {
|
|
199
|
+
return [
|
|
200
|
+
'# Credenciales de notificaciones Telegram — generado por swl-ses',
|
|
201
|
+
'# Este archivo vive en ~/.claude/notifications/ (fuera del repo, permisos 600)',
|
|
202
|
+
'# El token NO se commitea al repositorio.',
|
|
203
|
+
'# Para revocar: /swl:notificaciones disable',
|
|
204
|
+
'',
|
|
205
|
+
`TELEGRAM_BOT_TOKEN=${token}`,
|
|
206
|
+
`TELEGRAM_CHAT_ID=${chatId}`,
|
|
207
|
+
'',
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Validación de formato de credenciales Telegram
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Regex para validar el formato del token de bot de Telegram.
|
|
217
|
+
* Formato esperado: <digits>:<alphanum~35chars> — entregado por BotFather.
|
|
218
|
+
* Ejemplo válido: 7123456789:AAHkjdhsjkhd-sdfJKH987jklsd_xyzABCDEF
|
|
219
|
+
*
|
|
220
|
+
* NUNCA loggear el valor del token — solo longitud y booleano de validez.
|
|
221
|
+
*/
|
|
222
|
+
const RX_TOKEN_TELEGRAM = /^\d+:[A-Za-z0-9_-]{20,}$/;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Regex para validar el chat_id de Telegram.
|
|
226
|
+
* Puede ser positivo (chat privado) o negativo (grupo/supergrupo).
|
|
227
|
+
*/
|
|
228
|
+
const RX_CHAT_ID = /^-?\d+$/;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Valida el formato del token de Telegram sin loggear su valor.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} token
|
|
234
|
+
* @returns {boolean}
|
|
235
|
+
*/
|
|
236
|
+
function _validarFormatoToken(token) {
|
|
237
|
+
if (!token || typeof token !== 'string') return false;
|
|
238
|
+
return RX_TOKEN_TELEGRAM.test(token);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Valida el formato del chat_id de Telegram.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} chatId
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
function _validarFormatoChatId(chatId) {
|
|
248
|
+
if (!chatId || typeof chatId !== 'string') return false;
|
|
249
|
+
return RX_CHAT_ID.test(chatId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Crea un backup del .env existente antes de sobreescribirlo.
|
|
254
|
+
* El backup se almacena en el mismo directorio con timestamp Unix en el nombre.
|
|
255
|
+
* NUNCA loggea el contenido del .env — solo el path del backup.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} rutaEnv - Ruta del .env a respaldar.
|
|
258
|
+
* @returns {{ ok: boolean, rutaBackup?: string, error?: string }}
|
|
259
|
+
*/
|
|
260
|
+
function _respaldarEnvExistente(rutaEnv) {
|
|
261
|
+
try {
|
|
262
|
+
// Solo respaldar si existe y tiene contenido de credenciales válidas
|
|
263
|
+
if (!fs.existsSync(rutaEnv)) return { ok: true };
|
|
264
|
+
const contenido = fs.readFileSync(rutaEnv, 'utf8');
|
|
265
|
+
if (!contenido.includes('TELEGRAM_BOT_TOKEN=')) return { ok: true };
|
|
266
|
+
|
|
267
|
+
const ts = Date.now();
|
|
268
|
+
const rutaBackup = rutaEnv + `.swl-backup-${ts}`;
|
|
269
|
+
// Copiar con permisos 600 en POSIX
|
|
270
|
+
const writeOpts = { encoding: 'utf8' };
|
|
271
|
+
if (process.platform !== 'win32') writeOpts.mode = 0o600;
|
|
272
|
+
fs.writeFileSync(rutaBackup, contenido, writeOpts);
|
|
273
|
+
|
|
274
|
+
// Loggear en init.log solo el path y timestamp — NUNCA el contenido
|
|
275
|
+
const logPath = path.join(DIR_NOTIFICACIONES, 'init.log');
|
|
276
|
+
const logLinea = `[${new Date(ts).toISOString()}] backup creado: ${rutaBackup}\n`;
|
|
277
|
+
try { fs.appendFileSync(logPath, logLinea, 'utf8'); } catch (_) {}
|
|
278
|
+
|
|
279
|
+
return { ok: true, rutaBackup };
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return { ok: false, error: err.message };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Lee credenciales de forma interactiva usando node:readline.
|
|
287
|
+
* Valida el formato antes de aceptar. Permite hasta MAX_REINTENTOS intentos.
|
|
288
|
+
* Muestra instrucciones cortas con links a BotFather y /quien.
|
|
289
|
+
*
|
|
290
|
+
* @returns {Promise<{ token: string, chatId: string } | null>} null si el
|
|
291
|
+
* usuario cancela o agota los reintentos.
|
|
292
|
+
*/
|
|
293
|
+
async function leerCredencialesInteractivo() {
|
|
294
|
+
const MAX_REINTENTOS = 3;
|
|
295
|
+
|
|
296
|
+
const rl = readline.createInterface({
|
|
297
|
+
input: process.stdin,
|
|
298
|
+
output: process.stdout,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const preguntar = (msg) => new Promise((resolve) => {
|
|
302
|
+
rl.question(msg, (resp) => resolve(resp.trim()));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(' Instrucciones para obtener las credenciales:');
|
|
308
|
+
console.log(' 1. Crea un bot en https://t.me/BotFather con /newbot');
|
|
309
|
+
console.log(' 2. Copia el token que te entrega.');
|
|
310
|
+
console.log(' 3. Envía /quien al bot para obtener tu chat_id.');
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(' El token NO se commitea al repositorio.');
|
|
313
|
+
console.log(' Se almacena en ~/.claude/notifications/.env (fuera del repo, permisos 600).');
|
|
314
|
+
console.log('');
|
|
315
|
+
|
|
316
|
+
// Leer y validar token con retry
|
|
317
|
+
let token = null;
|
|
318
|
+
for (let intento = 1; intento <= MAX_REINTENTOS; intento++) {
|
|
319
|
+
const valorToken = await preguntar(' TELEGRAM_BOT_TOKEN: ');
|
|
320
|
+
if (!valorToken) {
|
|
321
|
+
console.log(' Token vacío — cancelando.');
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (_validarFormatoToken(valorToken)) {
|
|
325
|
+
token = valorToken;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const restantes = MAX_REINTENTOS - intento;
|
|
329
|
+
if (restantes > 0) {
|
|
330
|
+
console.log(` Token con formato inválido. Esperado: <digits>:<alphanum> (~46 chars de BotFather)`);
|
|
331
|
+
console.log(` Ejemplo: 7123456789:AAHkjdhsjkhd-sdfJKH987jklsd_xyzABCDEF`);
|
|
332
|
+
console.log(` (longitud=${valorToken.length}, formato_valido=false) — ${restantes} intento(s) restante(s).`);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(` Token con formato inválido (longitud=${valorToken.length}, formato_valido=false). Máximo de reintentos alcanzado.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!token) return null;
|
|
338
|
+
|
|
339
|
+
// Leer y validar chatId con retry
|
|
340
|
+
let chatId = null;
|
|
341
|
+
for (let intento = 1; intento <= MAX_REINTENTOS; intento++) {
|
|
342
|
+
const valorChatId = await preguntar(' TELEGRAM_CHAT_ID: ');
|
|
343
|
+
if (!valorChatId) {
|
|
344
|
+
console.log(' Chat ID vacío — cancelando.');
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
if (_validarFormatoChatId(valorChatId)) {
|
|
348
|
+
chatId = valorChatId;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const restantes = MAX_REINTENTOS - intento;
|
|
352
|
+
if (restantes > 0) {
|
|
353
|
+
console.log(` Chat ID con formato inválido. Esperado: número entero positivo o negativo.`);
|
|
354
|
+
console.log(` Ejemplo: -1001234567890 o 123456789`);
|
|
355
|
+
console.log(` (formato_valido=false) — ${restantes} intento(s) restante(s).`);
|
|
356
|
+
} else {
|
|
357
|
+
console.log(` Chat ID con formato inválido. Máximo de reintentos alcanzado.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!chatId) return null;
|
|
361
|
+
|
|
362
|
+
return { token, chatId };
|
|
363
|
+
} finally {
|
|
364
|
+
rl.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Genera el JSON de baseline-hooks: la lista de hooks que el hook
|
|
370
|
+
* SessionStart usará para restaurar settings.json si se pierden.
|
|
371
|
+
*
|
|
372
|
+
* SIEMPRE usa paths absolutos a ~/.claude/hooks/ para que los hooks
|
|
373
|
+
* funcionen desde cualquier directorio de trabajo (cwd).
|
|
374
|
+
*
|
|
375
|
+
* @param {string} [hooksGlobalDirOverride] - Para tests: sobreescribir el dir global.
|
|
376
|
+
* @returns {object} Objeto JSON listo para escribir.
|
|
377
|
+
*/
|
|
378
|
+
function _generarBaselineHooks(hooksGlobalDirOverride) {
|
|
379
|
+
const hooksDir = hooksGlobalDirOverride || HOOKS_GLOBAL_DIR;
|
|
380
|
+
const baseline = { hooks: {} };
|
|
381
|
+
|
|
382
|
+
for (const h of HOOKS_SWL_NOTIFICACIONES) {
|
|
383
|
+
if (!baseline.hooks[h.evento]) {
|
|
384
|
+
baseline.hooks[h.evento] = [];
|
|
385
|
+
}
|
|
386
|
+
// Evitar duplicar el mismo archivo en el mismo evento
|
|
387
|
+
const yaExiste = baseline.hooks[h.evento].some(
|
|
388
|
+
entry => (entry.hooks || []).some(x => x.command && x.command.includes(h.archivo))
|
|
389
|
+
);
|
|
390
|
+
if (!yaExiste) {
|
|
391
|
+
// REM-B: SIEMPRE path absoluto — nunca path relativo
|
|
392
|
+
const rutaAbs = path.join(hooksDir, h.archivo).replace(/\\/g, '/');
|
|
393
|
+
const comandoNode = `node "${rutaAbs}"`;
|
|
394
|
+
baseline.hooks[h.evento].push({
|
|
395
|
+
matcher: h.matcher,
|
|
396
|
+
hooks: [{ type: 'command', command: comandoNode }],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return baseline;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// API pública
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Inicializa la configuración de notificaciones Telegram.
|
|
410
|
+
*
|
|
411
|
+
* Flujo:
|
|
412
|
+
* 1. Verificar feature flag SWL_NOTIFICACIONES_TELEGRAM=0
|
|
413
|
+
* 2. Si no-TTY y sin flags headless → saltar
|
|
414
|
+
* 3. Preguntar opt-in (TTY) o usar credenciales de flags (headless)
|
|
415
|
+
* 4. Si respuesta n → omitir sin tocar nada
|
|
416
|
+
* 5. Obtener credenciales (interactivo o flags)
|
|
417
|
+
* 6. Verificar seguridad de ruta del .env
|
|
418
|
+
* 7. Crear ~/.claude/notifications/ si no existe
|
|
419
|
+
* 8. Escribir .env con modo 600 en POSIX
|
|
420
|
+
* 9. Generar baseline-hooks.json
|
|
421
|
+
* 10. Merge en ~/.claude/settings.json (backup + validación + rollback)
|
|
422
|
+
*
|
|
423
|
+
* @param {object} opciones
|
|
424
|
+
* @param {string} [opciones.token] - Token provisto por flag (modo headless).
|
|
425
|
+
* @param {string} [opciones.chatId] - Chat ID provisto por flag (modo headless).
|
|
426
|
+
* @param {boolean} [opciones.esTty] - Si el proceso corre en TTY.
|
|
427
|
+
* @param {boolean} [opciones.omitir] - Forzar omisión sin preguntar.
|
|
428
|
+
* @param {boolean} [opciones.sobreescribir] - Sobreescribir .env existente sin preguntar.
|
|
429
|
+
* @param {boolean} [opciones.dryRun] - Mostrar qué se haría sin tocar nada.
|
|
430
|
+
* @param {string} [opciones.hooksDir] - Directorio de hooks instalados.
|
|
431
|
+
* @param {string} [opciones._settingsPathOverride] - EXCLUSIVO para tests: ruta de settings.json
|
|
432
|
+
* alternativa. El prefijo _ señala que es parámetro de test — producción NUNCA lo pasa.
|
|
433
|
+
* Evita que los tests escriban al ~/.claude/settings.json REAL del usuario. (Fix Slice 015-B)
|
|
434
|
+
* @param {string} [opciones._envPathOverride] - EXCLUSIVO para tests: ruta de .env alternativa.
|
|
435
|
+
* El prefijo _ señala que es parámetro de test — producción NUNCA lo pasa.
|
|
436
|
+
* Evita que los tests escriban al ~/.claude/notifications/.env REAL del usuario. (Fix v5.12.2)
|
|
437
|
+
* @returns {Promise<{ resultado: string, detalle?: string }>}
|
|
438
|
+
* resultado: 'completado' | 'omitido' | 'error'
|
|
439
|
+
*/
|
|
440
|
+
async function init(opciones = {}) {
|
|
441
|
+
const {
|
|
442
|
+
token: tokenFlag = null,
|
|
443
|
+
chatId: chatIdFlag = null,
|
|
444
|
+
esTty: esTty = !!process.stdin.isTTY,
|
|
445
|
+
omitir: omitir = false,
|
|
446
|
+
sobreescribir: sobreescribir = false,
|
|
447
|
+
dryRun: dryRun = false,
|
|
448
|
+
hooksDir: hooksDir = null,
|
|
449
|
+
// Overrides para tests — permiten inyectar directorios temporales
|
|
450
|
+
_hooksOrigenOverride: _hooksOrigenOverride = null,
|
|
451
|
+
_hooksGlobalDirOverride: _hooksGlobalDirOverride = null,
|
|
452
|
+
// EXCLUSIVO para tests: ruta alternativa de settings.json — ver Fix Slice 015-B
|
|
453
|
+
_settingsPathOverride: _settingsPathOverride = null,
|
|
454
|
+
// EXCLUSIVO para tests: ruta alternativa del .env — ver Fix v5.12.2
|
|
455
|
+
_envPathOverride: _envPathOverride = null,
|
|
456
|
+
} = opciones;
|
|
457
|
+
|
|
458
|
+
// Ruta efectiva del .env: override para tests, canónica en producción
|
|
459
|
+
const envPath = _envPathOverride || RUTA_ENV;
|
|
460
|
+
|
|
461
|
+
// Feature flag de emergencia — desactivación sin tocar settings
|
|
462
|
+
if (process.env.SWL_NOTIFICACIONES_TELEGRAM === '0') {
|
|
463
|
+
console.log(' [notificaciones] SWL_NOTIFICACIONES_TELEGRAM=0 — omitido por variable de entorno.');
|
|
464
|
+
return { resultado: 'omitido', detalle: 'feature flag desactivado' };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (omitir) {
|
|
468
|
+
return { resultado: 'omitido', detalle: 'omisión forzada' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const modoHeadless = tokenFlag !== null && chatIdFlag !== null;
|
|
472
|
+
|
|
473
|
+
// En modo no-TTY sin flags headless → saltar automáticamente
|
|
474
|
+
if (!esTty && !modoHeadless) {
|
|
475
|
+
console.log(' [notificaciones] omitido en modo no-interactivo (usar /swl:notificaciones init para activarlo después).');
|
|
476
|
+
return { resultado: 'omitido', detalle: 'modo no-TTY sin flags headless' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let token = tokenFlag;
|
|
480
|
+
let chatId = chatIdFlag;
|
|
481
|
+
|
|
482
|
+
// Modo interactivo (TTY, sin flags headless)
|
|
483
|
+
if (!modoHeadless && esTty) {
|
|
484
|
+
// Detectar .env preexistente
|
|
485
|
+
if (fs.existsSync(envPath) && !sobreescribir) {
|
|
486
|
+
console.log('');
|
|
487
|
+
console.log(' [notificaciones] Ya existe ~/.claude/notifications/.env con credenciales.');
|
|
488
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
489
|
+
const reusar = await new Promise((resolve) => {
|
|
490
|
+
rl2.question(' ¿Reusar las credenciales existentes? [S/n] ', (resp) => {
|
|
491
|
+
rl2.close();
|
|
492
|
+
const r = resp.trim().toLowerCase();
|
|
493
|
+
resolve(r === '' || r === 's' || r === 'si' || r === 'sí' || r === 'y');
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
if (reusar) {
|
|
498
|
+
// Solo hacer merge en settings global sin reescribir .env
|
|
499
|
+
// REM-B: pasar override para que mergeHooksGlobales use ruta absoluta
|
|
500
|
+
return _soloMergeSettings(_hooksGlobalDirOverride, dryRun, _settingsPathOverride);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Preguntar opt-in
|
|
505
|
+
console.log('');
|
|
506
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
507
|
+
const habilitarNotif = await new Promise((resolve) => {
|
|
508
|
+
rl.question(
|
|
509
|
+
' ¿Habilitar notificaciones Telegram al final de cada turno? [s/N] ',
|
|
510
|
+
(resp) => {
|
|
511
|
+
rl.close();
|
|
512
|
+
const r = resp.trim().toLowerCase();
|
|
513
|
+
resolve(r === 's' || r === 'si' || r === 'sí' || r === 'y');
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (!habilitarNotif) {
|
|
519
|
+
console.log(' [notificaciones] omitidas.');
|
|
520
|
+
return { resultado: 'omitido', detalle: 'usuario respondió n' };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const creds = await leerCredencialesInteractivo();
|
|
524
|
+
if (!creds) {
|
|
525
|
+
return { resultado: 'omitido', detalle: 'usuario canceló ingreso de credenciales' };
|
|
526
|
+
}
|
|
527
|
+
token = creds.token;
|
|
528
|
+
chatId = creds.chatId;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Headless con .env preexistente: reusar silenciosamente si no se pide sobreescribir
|
|
532
|
+
if (modoHeadless && fs.existsSync(envPath) && !sobreescribir) {
|
|
533
|
+
console.log(' [notificaciones] Reusando .env existente (usar --sobreescribir para reemplazar).');
|
|
534
|
+
// REM-B: pasar override para que mergeHooksGlobales use ruta absoluta
|
|
535
|
+
return _soloMergeSettings(_hooksGlobalDirOverride, dryRun, _settingsPathOverride);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Validar que tenemos credenciales
|
|
539
|
+
if (!token || !chatId) {
|
|
540
|
+
return { resultado: 'error', detalle: 'credenciales incompletas (token y chatId son requeridos)' };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Validar formato de token — NUNCA loggear el valor
|
|
544
|
+
if (!_validarFormatoToken(token)) {
|
|
545
|
+
const msg = `Token con formato inválido (longitud=${token.length}, formato_valido=false). Esperado: <digits>:<alphanum> (~46 chars de BotFather).`;
|
|
546
|
+
if (modoHeadless) {
|
|
547
|
+
process.stderr.write(`[notificaciones] Error: ${msg}\n`);
|
|
548
|
+
return { resultado: 'error', detalle: msg };
|
|
549
|
+
}
|
|
550
|
+
return { resultado: 'error', detalle: msg };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Validar formato de chatId
|
|
554
|
+
if (!_validarFormatoChatId(chatId)) {
|
|
555
|
+
const msg = `Chat ID con formato inválido. Esperado: número entero positivo o negativo (ej: -1001234567890).`;
|
|
556
|
+
if (modoHeadless) {
|
|
557
|
+
process.stderr.write(`[notificaciones] Error: ${msg}\n`);
|
|
558
|
+
return { resultado: 'error', detalle: msg };
|
|
559
|
+
}
|
|
560
|
+
return { resultado: 'error', detalle: msg };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Verificar seguridad de ruta — .env NUNCA dentro del repo
|
|
564
|
+
// En modo test con _envPathOverride, saltamos la validación de ruta canónica
|
|
565
|
+
// porque el override apunta a un tmpDir fuera de DIR_NOTIFICACIONES (eso es correcto).
|
|
566
|
+
if (!_envPathOverride && !_esRutaSegura(envPath)) {
|
|
567
|
+
return {
|
|
568
|
+
resultado: 'error',
|
|
569
|
+
detalle: `Ruta insegura detectada: ${envPath} no está dentro de ${DIR_NOTIFICACIONES}. Abortando.`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (dryRun) {
|
|
574
|
+
console.log('');
|
|
575
|
+
console.log(' [dry-run] Archivos que se crearían/modificarían:');
|
|
576
|
+
console.log(` COPIAR ${HOOKS_A_COPIAR.length} hook(s) → ~/.claude/hooks/`);
|
|
577
|
+
console.log(` CREAR ${envPath} (modo 600 en POSIX)`);
|
|
578
|
+
console.log(` CREAR ${RUTA_BASELINE}`);
|
|
579
|
+
console.log(` MERGE ${RUTA_SETTINGS_GLOBAL} (backup antes de modificar)`);
|
|
580
|
+
console.log('');
|
|
581
|
+
return { resultado: 'omitido', detalle: 'dry-run: sin cambios aplicados' };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// REM-A: copiar hooks al directorio global ANTES de registrar en settings.
|
|
585
|
+
// Garantiza que Claude Code siempre encuentre los archivos en ~/.claude/hooks/
|
|
586
|
+
// independientemente del proyecto activo al ejecutarse el Stop hook.
|
|
587
|
+
try {
|
|
588
|
+
const copiaRes = _copiarHooksGlobales(_hooksOrigenOverride, _hooksGlobalDirOverride);
|
|
589
|
+
console.log(` [notificaciones] ${copiaRes.archivos.length} hook(s) copiados a: ${copiaRes.directorio}`);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(` ! [notificaciones] Error copiando hooks: ${err.message}`);
|
|
592
|
+
return { resultado: 'error', detalle: `No se pudieron copiar los hooks globales: ${err.message}` };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Crear directorio si no existe
|
|
596
|
+
if (!fs.existsSync(DIR_NOTIFICACIONES)) {
|
|
597
|
+
fs.mkdirSync(DIR_NOTIFICACIONES, { recursive: true });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Respaldar .env preexistente ANTES de sobreescribir (REM-2)
|
|
601
|
+
// Protege contra el incidente del 2026-04-24 donde init() sobreescribió
|
|
602
|
+
// credenciales válidas durante una validación manual del flujo.
|
|
603
|
+
const backupRes = _respaldarEnvExistente(envPath);
|
|
604
|
+
if (!backupRes.ok) {
|
|
605
|
+
console.error(` ! [notificaciones] Advertencia: no se pudo crear backup del .env: ${backupRes.error}`);
|
|
606
|
+
// No es fatal — continuamos con la escritura
|
|
607
|
+
} else if (backupRes.rutaBackup) {
|
|
608
|
+
console.log(` [notificaciones] Backup del .env anterior: ${backupRes.rutaBackup}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Escribir .env — atómico, con permisos 0o600 aplicados desde la creación (REM-1)
|
|
612
|
+
// Elimina la race condition entre escritura y chmod posterior.
|
|
613
|
+
const contenidoEnv = _generarContenidoEnv(token, chatId);
|
|
614
|
+
atomicWriteSync(envPath, contenidoEnv, 'utf8', { mode: 0o600 });
|
|
615
|
+
|
|
616
|
+
console.log(` [notificaciones] Token configurado: ${_ocultarToken(token)}`);
|
|
617
|
+
console.log(` [notificaciones] Chat ID configurado: ${chatId.slice(0, -4).replace(/./g, '*')}${chatId.slice(-4)}`);
|
|
618
|
+
console.log(` [notificaciones] .env escrito en: ${envPath}`);
|
|
619
|
+
|
|
620
|
+
// Generar baseline-hooks.json — REM-B: siempre usa path absoluto a ~/.claude/hooks/
|
|
621
|
+
const baseline = _generarBaselineHooks(_hooksGlobalDirOverride);
|
|
622
|
+
atomicWriteJSON(RUTA_BASELINE, baseline);
|
|
623
|
+
console.log(` [notificaciones] baseline-hooks.json escrito en: ${RUTA_BASELINE}`);
|
|
624
|
+
|
|
625
|
+
// Merge en settings global
|
|
626
|
+
const mergeResult = await _soloMergeSettings(_hooksGlobalDirOverride, false, _settingsPathOverride);
|
|
627
|
+
if (mergeResult.resultado === 'error') {
|
|
628
|
+
return mergeResult;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
resultado: 'completado',
|
|
633
|
+
detalle: `token=${_ocultarToken(token)}, chatId configurado, baseline y settings actualizados`,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Hace el merge de hooks SWL en settings global sin tocar el .env.
|
|
639
|
+
* Usado cuando el .env ya existe y solo se necesita restaurar la entrada en settings.
|
|
640
|
+
*
|
|
641
|
+
* @param {string|null} hooksGlobalDirOverride - Override de directorio para tests; si null usa HOOKS_GLOBAL_DIR.
|
|
642
|
+
* @param {boolean} dryRun
|
|
643
|
+
* @param {string|null} settingsPathOverride - EXCLUSIVO para tests: sobreescribir ruta del settings.json.
|
|
644
|
+
* En producción, pasar null (usa RUTA_SETTINGS_GLOBAL). El guion bajo en los llamadores
|
|
645
|
+
* señala que es parámetro de test únicamente.
|
|
646
|
+
* @returns {Promise<{ resultado: string, detalle?: string }>}
|
|
647
|
+
*/
|
|
648
|
+
async function _soloMergeSettings(hooksGlobalDirOverride, dryRun, settingsPathOverride) {
|
|
649
|
+
const settingsPath = settingsPathOverride || RUTA_SETTINGS_GLOBAL;
|
|
650
|
+
|
|
651
|
+
if (dryRun) {
|
|
652
|
+
console.log(` [dry-run] MERGE ${settingsPath} (backup antes de modificar)`);
|
|
653
|
+
return { resultado: 'omitido', detalle: 'dry-run: merge de settings no aplicado' };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// REM-B: siempre usar directorio global absoluto — nunca ruta relativa
|
|
657
|
+
const hooksDir = hooksGlobalDirOverride || HOOKS_GLOBAL_DIR;
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const entries = HOOKS_SWL_NOTIFICACIONES.map(h => ({
|
|
661
|
+
evento: h.evento,
|
|
662
|
+
matcher: h.matcher,
|
|
663
|
+
archivo: h.archivo,
|
|
664
|
+
hooksDir: hooksDir,
|
|
665
|
+
}));
|
|
666
|
+
|
|
667
|
+
const res = hooksSettings.mergeHooksGlobales(settingsPath, entries);
|
|
668
|
+
if (res.error) {
|
|
669
|
+
console.error(` ! [notificaciones] Error en merge de settings: ${res.error}`);
|
|
670
|
+
return { resultado: 'error', detalle: res.error };
|
|
671
|
+
}
|
|
672
|
+
const detalleMigrados = res.migrados > 0 ? `, ${res.migrados} migrados a ruta absoluta` : '';
|
|
673
|
+
console.log(` [notificaciones] settings.json global actualizado (${res.agregados} hook(s) agregados${detalleMigrados}, ${res.preservados} preservados).`);
|
|
674
|
+
return { resultado: 'completado', detalle: `settings actualizado (${res.agregados} hooks)` };
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error(` ! [notificaciones] Error inesperado en merge de settings: ${err.message}`);
|
|
677
|
+
return { resultado: 'error', detalle: err.message };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Retorna el estado actual de la configuración de notificaciones.
|
|
683
|
+
* NUNCA incluye el valor del token — solo metadata.
|
|
684
|
+
*
|
|
685
|
+
* @returns {object}
|
|
686
|
+
*/
|
|
687
|
+
function status() {
|
|
688
|
+
const envExiste = fs.existsSync(RUTA_ENV);
|
|
689
|
+
let tokenConfigurado = false;
|
|
690
|
+
let chatIdConfigurado = false;
|
|
691
|
+
let chatIdParcial = null;
|
|
692
|
+
let proyectosSilenciados = [];
|
|
693
|
+
|
|
694
|
+
if (envExiste) {
|
|
695
|
+
try {
|
|
696
|
+
const contenido = fs.readFileSync(RUTA_ENV, 'utf8');
|
|
697
|
+
for (const linea of contenido.split(/\r?\n/)) {
|
|
698
|
+
const recortada = linea.trim();
|
|
699
|
+
if (!recortada || recortada.startsWith('#')) continue;
|
|
700
|
+
const posIgual = recortada.indexOf('=');
|
|
701
|
+
if (posIgual === -1) continue;
|
|
702
|
+
const nombre = recortada.slice(0, posIgual).trim();
|
|
703
|
+
const valor = recortada.slice(posIgual + 1).trim().replace(/^["']|["']$/g, '');
|
|
704
|
+
if (nombre === 'TELEGRAM_BOT_TOKEN' && valor) {
|
|
705
|
+
tokenConfigurado = true;
|
|
706
|
+
// NUNCA incluir el token en el retorno
|
|
707
|
+
}
|
|
708
|
+
if (nombre === 'TELEGRAM_CHAT_ID' && valor) {
|
|
709
|
+
chatIdConfigurado = true;
|
|
710
|
+
// Solo últimos 4 dígitos
|
|
711
|
+
chatIdParcial = valor.length > 4
|
|
712
|
+
? '...' + valor.slice(-4)
|
|
713
|
+
: valor;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch (_) {
|
|
717
|
+
// Si no se puede leer, dejar flags en false
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Leer proyectos silenciados
|
|
722
|
+
const rutaMuted = path.join(DIR_NOTIFICACIONES, 'muted.json');
|
|
723
|
+
try {
|
|
724
|
+
if (fs.existsSync(rutaMuted)) {
|
|
725
|
+
const muted = JSON.parse(fs.readFileSync(rutaMuted, 'utf8'));
|
|
726
|
+
if (Array.isArray(muted)) proyectosSilenciados = muted;
|
|
727
|
+
}
|
|
728
|
+
} catch (_) { /* muted.json corrupto — ignorar */ }
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
envExiste,
|
|
732
|
+
tokenConfigurado,
|
|
733
|
+
chatIdConfigurado,
|
|
734
|
+
chatIdParcial, // últimos 4 dígitos o null
|
|
735
|
+
proyectosSilenciados,
|
|
736
|
+
// Campos reservados para Slices futuros
|
|
737
|
+
botDaemonActivo: null, // Slice 006
|
|
738
|
+
autostartInstalado: null, // Slice 008
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Deshabilita las notificaciones Telegram:
|
|
744
|
+
* - Quita entries SWL de ~/.claude/settings.json (preserva ajenos)
|
|
745
|
+
* - Si conservarEnv=false, elimina el .env
|
|
746
|
+
* - NO detiene el daemon (Slice 006)
|
|
747
|
+
*
|
|
748
|
+
* @param {object} [opciones]
|
|
749
|
+
* @param {boolean} [opciones.confirmar=false] - Si ya se confirmó por UI.
|
|
750
|
+
* @param {boolean} [opciones.conservarEnv=true] - Si conservar el .env.
|
|
751
|
+
* @param {string|null} [opciones._settingsPathOverride] - EXCLUSIVO para tests: ruta alternativa del settings.json.
|
|
752
|
+
* @param {string|null} [opciones._envPathOverride] - EXCLUSIVO para tests: ruta alternativa del .env.
|
|
753
|
+
* El prefijo _ señala que es parámetro de test — producción NUNCA lo pasa. (Fix v5.12.2)
|
|
754
|
+
* @returns {{ resultado: string, detalle?: string }}
|
|
755
|
+
*/
|
|
756
|
+
function disable(opciones = {}) {
|
|
757
|
+
const {
|
|
758
|
+
confirmar = false,
|
|
759
|
+
conservarEnv = true,
|
|
760
|
+
_settingsPathOverride = null, // EXCLUSIVO para tests — ver Fix Slice 015-B
|
|
761
|
+
_envPathOverride = null, // EXCLUSIVO para tests — ver Fix v5.12.2
|
|
762
|
+
} = opciones;
|
|
763
|
+
|
|
764
|
+
if (!confirmar) {
|
|
765
|
+
return {
|
|
766
|
+
resultado: 'error',
|
|
767
|
+
detalle: 'Se requiere confirmar (pasar confirmar: true). Ejecuta /swl:notificaciones disable para el flujo interactivo.',
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const settingsPath = _settingsPathOverride || RUTA_SETTINGS_GLOBAL;
|
|
772
|
+
const envPath = _envPathOverride || RUTA_ENV;
|
|
773
|
+
|
|
774
|
+
// Quitar entries SWL del settings global
|
|
775
|
+
try {
|
|
776
|
+
const res = hooksSettings.desregistrarHooksGlobales(settingsPath);
|
|
777
|
+
console.log(` [notificaciones] ${res.eliminados} hook(s) SWL eliminados de settings.json (${res.preservados} preservados).`);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
console.error(` ! [notificaciones] Error al quitar hooks: ${err.message}`);
|
|
780
|
+
return { resultado: 'error', detalle: err.message };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Eliminar .env si se pide
|
|
784
|
+
if (!conservarEnv && fs.existsSync(envPath)) {
|
|
785
|
+
try {
|
|
786
|
+
fs.unlinkSync(envPath);
|
|
787
|
+
console.log(` [notificaciones] .env eliminado.`);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error(` ! [notificaciones] No se pudo eliminar .env: ${err.message}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return { resultado: 'completado', detalle: 'hooks removidos de settings global' };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
// Gestión del daemon bidireccional (bot.*)
|
|
798
|
+
// ---------------------------------------------------------------------------
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Lee el PID almacenado en el archivo de PID del daemon.
|
|
802
|
+
* Retorna null si no existe o no es un número válido.
|
|
803
|
+
*
|
|
804
|
+
* @returns {number|null}
|
|
805
|
+
*/
|
|
806
|
+
function _leerPid() {
|
|
807
|
+
try {
|
|
808
|
+
if (!fs.existsSync(RUTA_PID_DAEMON)) return null;
|
|
809
|
+
const contenido = fs.readFileSync(RUTA_PID_DAEMON, 'utf8').trim();
|
|
810
|
+
const pid = parseInt(contenido, 10);
|
|
811
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
812
|
+
} catch (_) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Verifica si un proceso con el PID dado está activo.
|
|
819
|
+
* Usa `process.kill(pid, 0)` que lanza si el proceso no existe.
|
|
820
|
+
*
|
|
821
|
+
* @param {number} pid
|
|
822
|
+
* @returns {boolean}
|
|
823
|
+
*/
|
|
824
|
+
function _procesoActivo(pid) {
|
|
825
|
+
if (!pid) return false;
|
|
826
|
+
try {
|
|
827
|
+
process.kill(pid, 0);
|
|
828
|
+
return true;
|
|
829
|
+
} catch (_) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Retorna si el daemon del bot está activo según el archivo PID.
|
|
836
|
+
*
|
|
837
|
+
* @returns {{ activo: boolean, pid: number|null }}
|
|
838
|
+
*/
|
|
839
|
+
function botStatus() {
|
|
840
|
+
const pid = _leerPid();
|
|
841
|
+
const activo = _procesoActivo(pid);
|
|
842
|
+
|
|
843
|
+
// Limpiar PID obsoleto si el proceso ya no existe
|
|
844
|
+
if (pid && !activo) {
|
|
845
|
+
try { fs.unlinkSync(RUTA_PID_DAEMON); } catch (_) {}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { activo, pid: activo ? pid : null };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Inicia el daemon del bot en background.
|
|
853
|
+
*
|
|
854
|
+
* El daemon se inicia desacoplado del proceso actual (detached: true, stdio: 'ignore')
|
|
855
|
+
* para que sobreviva al cierre del proceso padre. El PID se escribe en bot.pid.
|
|
856
|
+
*
|
|
857
|
+
* @returns {{ ok: boolean, mensaje?: string, error?: string, pid?: number }}
|
|
858
|
+
*/
|
|
859
|
+
function botStart() {
|
|
860
|
+
// Verificar que el binario existe
|
|
861
|
+
if (!fs.existsSync(RUTA_BIN_DAEMON)) {
|
|
862
|
+
return { ok: false, error: `El daemon no existe: ${RUTA_BIN_DAEMON}` };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Verificar que no está ya corriendo
|
|
866
|
+
const { activo, pid: pidExistente } = botStatus();
|
|
867
|
+
if (activo) {
|
|
868
|
+
return { ok: true, mensaje: `El bot ya está corriendo (PID ${pidExistente}).` };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Crear directorio de logs si no existe
|
|
872
|
+
try {
|
|
873
|
+
if (!fs.existsSync(DIR_NOTIFICACIONES)) {
|
|
874
|
+
fs.mkdirSync(DIR_NOTIFICACIONES, { recursive: true });
|
|
875
|
+
}
|
|
876
|
+
} catch (err) {
|
|
877
|
+
return { ok: false, error: `No se pudo crear directorio de logs: ${err.message}` };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Iniciar el proceso desacoplado
|
|
881
|
+
let child;
|
|
882
|
+
try {
|
|
883
|
+
const stdoutLog = path.join(DIR_NOTIFICACIONES, 'bot.stdout.log');
|
|
884
|
+
const stderrLog = path.join(DIR_NOTIFICACIONES, 'bot.stderr.log');
|
|
885
|
+
const outFd = fs.openSync(stdoutLog, 'a');
|
|
886
|
+
const errFd = fs.openSync(stderrLog, 'a');
|
|
887
|
+
|
|
888
|
+
const { spawn } = require('node:child_process');
|
|
889
|
+
child = spawn(process.execPath, [RUTA_BIN_DAEMON], {
|
|
890
|
+
detached: true,
|
|
891
|
+
stdio: ['ignore', outFd, errFd],
|
|
892
|
+
});
|
|
893
|
+
child.unref();
|
|
894
|
+
fs.closeSync(outFd);
|
|
895
|
+
fs.closeSync(errFd);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
return { ok: false, error: `No se pudo iniciar el daemon: ${err.message}` };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const pid = child.pid;
|
|
901
|
+
|
|
902
|
+
// Escribir el PID en archivo
|
|
903
|
+
try {
|
|
904
|
+
fs.writeFileSync(RUTA_PID_DAEMON, String(pid), 'utf8');
|
|
905
|
+
} catch (err) {
|
|
906
|
+
return { ok: false, error: `Daemon iniciado (PID ${pid}) pero no se pudo escribir bot.pid: ${err.message}` };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return { ok: true, mensaje: `Bot iniciado (PID ${pid}).`, pid };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Detiene el daemon del bot enviando SIGTERM al PID almacenado.
|
|
914
|
+
* Elimina el archivo bot.pid al terminar.
|
|
915
|
+
*
|
|
916
|
+
* @returns {{ ok: boolean, mensaje?: string, error?: string }}
|
|
917
|
+
*/
|
|
918
|
+
function botStop() {
|
|
919
|
+
const { activo, pid } = botStatus();
|
|
920
|
+
|
|
921
|
+
if (!activo || !pid) {
|
|
922
|
+
return { ok: true, mensaje: 'El bot no estaba corriendo.' };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
process.kill(pid, 'SIGTERM');
|
|
927
|
+
} catch (err) {
|
|
928
|
+
return { ok: false, error: `No se pudo detener el proceso (PID ${pid}): ${err.message}` };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Eliminar el archivo PID
|
|
932
|
+
try {
|
|
933
|
+
if (fs.existsSync(RUTA_PID_DAEMON)) {
|
|
934
|
+
fs.unlinkSync(RUTA_PID_DAEMON);
|
|
935
|
+
}
|
|
936
|
+
} catch (_) {
|
|
937
|
+
// No es fatal si no se puede eliminar el PID
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return { ok: true, mensaje: `Bot detenido (PID ${pid}).` };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Reinicia el daemon: detiene si está corriendo y lo vuelve a iniciar.
|
|
945
|
+
*
|
|
946
|
+
* @returns {{ ok: boolean, mensaje?: string, error?: string, pid?: number }}
|
|
947
|
+
*/
|
|
948
|
+
function botRestart() {
|
|
949
|
+
const stopRes = botStop();
|
|
950
|
+
if (!stopRes.ok) {
|
|
951
|
+
return stopRes;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Pequeña espera síncrona para que el proceso termine de limpiar recursos
|
|
955
|
+
// Usar un loop de spin corto sin setTimeout (tests deterministas)
|
|
956
|
+
const inicio = Date.now();
|
|
957
|
+
while (Date.now() - inicio < 500) {
|
|
958
|
+
const { activo } = botStatus();
|
|
959
|
+
if (!activo) break;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return botStart();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Despacha la instalación del autostart al módulo del OS correspondiente.
|
|
967
|
+
*
|
|
968
|
+
* @returns {{ ok: boolean, mensaje?: string, error?: string }}
|
|
969
|
+
*/
|
|
970
|
+
function botEnableAutostart() {
|
|
971
|
+
const platform = process.platform;
|
|
972
|
+
|
|
973
|
+
if (platform === 'win32') {
|
|
974
|
+
const { install } = require('./autostart-windows');
|
|
975
|
+
return install();
|
|
976
|
+
}
|
|
977
|
+
if (platform === 'linux') {
|
|
978
|
+
const { install } = require('./autostart-linux');
|
|
979
|
+
return install();
|
|
980
|
+
}
|
|
981
|
+
if (platform === 'darwin') {
|
|
982
|
+
const { install } = require('./autostart-macos');
|
|
983
|
+
return install();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
error: `Plataforma no soportada para autostart: ${platform}. Soportadas: win32, linux, darwin.`,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Despacha la desinstalación del autostart al módulo del OS correspondiente.
|
|
994
|
+
*
|
|
995
|
+
* @returns {{ ok: boolean, mensaje?: string, error?: string }}
|
|
996
|
+
*/
|
|
997
|
+
function botDisableAutostart() {
|
|
998
|
+
const platform = process.platform;
|
|
999
|
+
|
|
1000
|
+
if (platform === 'win32') {
|
|
1001
|
+
const { uninstall } = require('./autostart-windows');
|
|
1002
|
+
return uninstall();
|
|
1003
|
+
}
|
|
1004
|
+
if (platform === 'linux') {
|
|
1005
|
+
const { uninstall } = require('./autostart-linux');
|
|
1006
|
+
return uninstall();
|
|
1007
|
+
}
|
|
1008
|
+
if (platform === 'darwin') {
|
|
1009
|
+
const { uninstall } = require('./autostart-macos');
|
|
1010
|
+
return uninstall();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
ok: false,
|
|
1015
|
+
error: `Plataforma no soportada para autostart: ${platform}. Soportadas: win32, linux, darwin.`,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Repara una instalación rota de hooks de Telegram.
|
|
1021
|
+
*
|
|
1022
|
+
* Ejecuta dos acciones correctivas:
|
|
1023
|
+
* 1. Re-copia los 7 archivos de hook a ~/.claude/hooks/ (idempotente).
|
|
1024
|
+
* 2. Llama a _soloMergeSettings() que invoca mergeHooksGlobales con el
|
|
1025
|
+
* mecanismo de migración REM-C: detecta entradas con ruta relativa del
|
|
1026
|
+
* mismo hook y las reemplaza por la ruta absoluta sin duplicar.
|
|
1027
|
+
*
|
|
1028
|
+
* Diseñado para usuarios que activaron notificaciones antes de v5.12.0 y
|
|
1029
|
+
* tienen entradas como `node hooks/notificacion-telegram.js` en settings.json.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {object} opciones
|
|
1032
|
+
* @param {string} [opciones._hooksOrigenOverride] - Override para tests.
|
|
1033
|
+
* @param {string} [opciones._hooksGlobalDirOverride] - Override para tests.
|
|
1034
|
+
* @param {string|null} [opciones._settingsPathOverride] - EXCLUSIVO para tests: ruta alternativa del settings.json.
|
|
1035
|
+
* @param {string|null} [opciones._envPathOverride] - EXCLUSIVO para tests: ruta alternativa del .env.
|
|
1036
|
+
* repair() actualmente no escribe el .env (solo re-copia hooks y merge settings),
|
|
1037
|
+
* pero el parámetro se acepta para uniformidad con init()/disable() y tests futuros.
|
|
1038
|
+
* (Fix v5.12.2)
|
|
1039
|
+
* @returns {Promise<{ resultado: string, detalle?: string }>}
|
|
1040
|
+
*/
|
|
1041
|
+
async function repair(opciones = {}) {
|
|
1042
|
+
const {
|
|
1043
|
+
_hooksOrigenOverride: _hooksOrigenOverride = null,
|
|
1044
|
+
_hooksGlobalDirOverride: _hooksGlobalDirOverride = null,
|
|
1045
|
+
_settingsPathOverride: _settingsPathOverride = null, // EXCLUSIVO para tests — ver Fix Slice 015-B
|
|
1046
|
+
// _envPathOverride aceptado por uniformidad — repair no escribe .env actualmente
|
|
1047
|
+
// eslint-disable-next-line no-unused-vars
|
|
1048
|
+
_envPathOverride: _envPathOverride = null, // EXCLUSIVO para tests — ver Fix v5.12.2
|
|
1049
|
+
} = opciones;
|
|
1050
|
+
|
|
1051
|
+
console.log(' [notificaciones] Iniciando reparación de hooks...');
|
|
1052
|
+
|
|
1053
|
+
// Paso 1: re-copiar hooks
|
|
1054
|
+
try {
|
|
1055
|
+
const copiaRes = _copiarHooksGlobales(_hooksOrigenOverride, _hooksGlobalDirOverride);
|
|
1056
|
+
console.log(` [notificaciones] ${copiaRes.archivos.length} hook(s) copiados a: ${copiaRes.directorio}`);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
console.error(` ! [notificaciones] Error copiando hooks: ${err.message}`);
|
|
1059
|
+
return { resultado: 'error', detalle: `No se pudieron copiar los hooks: ${err.message}` };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Paso 2: merge/migrar entradas en settings.json (REM-C: relativo → absoluto)
|
|
1063
|
+
const mergeResult = await _soloMergeSettings(_hooksGlobalDirOverride, false, _settingsPathOverride);
|
|
1064
|
+
if (mergeResult.resultado === 'error') {
|
|
1065
|
+
return mergeResult;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
console.log(' [notificaciones] Reparación completada. Hooks registrados con rutas absolutas.');
|
|
1069
|
+
return {
|
|
1070
|
+
resultado: 'completado',
|
|
1071
|
+
detalle: 'hooks copiados y settings migrados a rutas absolutas',
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
module.exports = {
|
|
1076
|
+
init,
|
|
1077
|
+
repair,
|
|
1078
|
+
status,
|
|
1079
|
+
disable,
|
|
1080
|
+
leerCredencialesInteractivo,
|
|
1081
|
+
// API del daemon
|
|
1082
|
+
botStart,
|
|
1083
|
+
botStop,
|
|
1084
|
+
botStatus,
|
|
1085
|
+
botRestart,
|
|
1086
|
+
botEnableAutostart,
|
|
1087
|
+
botDisableAutostart,
|
|
1088
|
+
// Exportar para tests
|
|
1089
|
+
_esRutaSegura,
|
|
1090
|
+
_ocultarToken,
|
|
1091
|
+
_generarContenidoEnv,
|
|
1092
|
+
_generarBaselineHooks,
|
|
1093
|
+
_copiarHooksGlobales,
|
|
1094
|
+
_leerPid,
|
|
1095
|
+
_procesoActivo,
|
|
1096
|
+
_validarFormatoToken,
|
|
1097
|
+
_validarFormatoChatId,
|
|
1098
|
+
_respaldarEnvExistente,
|
|
1099
|
+
RUTA_ENV,
|
|
1100
|
+
RUTA_BASELINE,
|
|
1101
|
+
RUTA_SETTINGS_GLOBAL,
|
|
1102
|
+
DIR_NOTIFICACIONES,
|
|
1103
|
+
RUTA_PID_DAEMON,
|
|
1104
|
+
RUTA_BIN_DAEMON,
|
|
1105
|
+
HOOKS_GLOBAL_DIR,
|
|
1106
|
+
HOOKS_A_COPIAR,
|
|
1107
|
+
};
|