@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const ENV_PATH = path.resolve(process.cwd(), '.env');
8
+ const MANAGED_BLOCK_START = '# >>> CORE_AI_MODE_MANAGED >>>';
9
+ const MANAGED_BLOCK_END = '# <<< CORE_AI_MODE_MANAGED <<<';
10
+
11
+ const CORE_AI_FLAGS = ['AI_LEARNING_WORKER_ENABLED', 'AI_HELP_CONTINUOUS_LEARNING_ENABLED', 'COMMAND_CONFIG_ENRICHMENT_WORKER_ENABLED', 'GLOBAL_HELP_ENABLE_WRAPPER_LLM_FALLBACK', 'MODULE_AI_HELP_ENABLE_LLM', 'ADMIN_AI_HELP_ENABLE_LLM', 'AI_AI_HELP_ENABLE_LLM', 'GAME_AI_HELP_ENABLE_LLM', 'MENU_AI_HELP_ENABLE_LLM', 'PLAY_AI_HELP_ENABLE_LLM', 'QUOTE_AI_HELP_ENABLE_LLM', 'RPG_POKEMON_AI_HELP_ENABLE_LLM', 'STATS_AI_HELP_ENABLE_LLM', 'STICKER_AI_HELP_ENABLE_LLM', 'STICKER_PACK_AI_HELP_ENABLE_LLM', 'SYSTEM_METRICS_AI_HELP_ENABLE_LLM', 'TIKTOK_AI_HELP_ENABLE_LLM', 'USER_AI_HELP_ENABLE_LLM', 'WAIFUPICS_AI_HELP_ENABLE_LLM'];
12
+
13
+ const parseArgs = (argv = []) => {
14
+ const args = [...argv];
15
+ let mode = 'status';
16
+ let restart = true;
17
+ let pm2Name = process.env.CORE_AI_PM2_NAME || 'omnizap-system-production';
18
+
19
+ if (args[0] && !args[0].startsWith('--')) {
20
+ mode = String(args.shift()).trim().toLowerCase();
21
+ }
22
+
23
+ for (const arg of args) {
24
+ if (arg === '--no-restart') {
25
+ restart = false;
26
+ continue;
27
+ }
28
+ if (arg.startsWith('--pm2-name=')) {
29
+ const value = String(arg.split('=').slice(1).join('=')).trim();
30
+ if (value) pm2Name = value;
31
+ }
32
+ }
33
+
34
+ return {
35
+ mode,
36
+ restart,
37
+ pm2Name,
38
+ };
39
+ };
40
+
41
+ const parseDotEnvEffectiveMap = (content) => {
42
+ const map = new Map();
43
+ const lines = String(content || '').split(/\r?\n/);
44
+ const keyRegex = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
45
+
46
+ for (const line of lines) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith('#')) continue;
49
+ const match = line.match(keyRegex);
50
+ if (!match) continue;
51
+ map.set(match[1], String(match[2] || '').trim());
52
+ }
53
+
54
+ return map;
55
+ };
56
+
57
+ const removeManagedBlock = (content) => {
58
+ const lines = String(content || '').split(/\r?\n/);
59
+ const startIndex = lines.findIndex((line) => line.trim() === MANAGED_BLOCK_START);
60
+ if (startIndex < 0) return content;
61
+ const endIndex = lines.findIndex((line, index) => index > startIndex && line.trim() === MANAGED_BLOCK_END);
62
+ if (endIndex < 0) {
63
+ return `${lines.slice(0, startIndex).join('\n').trimEnd()}\n`;
64
+ }
65
+ const nextLines = [...lines.slice(0, startIndex), ...lines.slice(endIndex + 1)];
66
+ return `${nextLines.join('\n').trimEnd()}\n`;
67
+ };
68
+
69
+ const buildManagedBlock = (value) => {
70
+ const lines = [MANAGED_BLOCK_START, '# Gerenciado por scripts/core-ai-mode.mjs', ...CORE_AI_FLAGS.map((key) => `${key}=${value}`), MANAGED_BLOCK_END];
71
+ return `${lines.join('\n')}\n`;
72
+ };
73
+
74
+ const writeModeToEnv = async (modeValue) => {
75
+ const current = await fs.readFile(ENV_PATH, 'utf8');
76
+ const withoutManaged = removeManagedBlock(current);
77
+ const next = withoutManaged.trimEnd().length > 0 ? `${withoutManaged.trimEnd()}\n\n${buildManagedBlock(modeValue)}` : `${buildManagedBlock(modeValue)}`;
78
+ await fs.writeFile(ENV_PATH, next, 'utf8');
79
+ };
80
+
81
+ const computeStatus = async () => {
82
+ const content = await fs.readFile(ENV_PATH, 'utf8');
83
+ const map = parseDotEnvEffectiveMap(content);
84
+ const values = CORE_AI_FLAGS.map((key) => {
85
+ const raw = String(map.get(key) ?? '')
86
+ .trim()
87
+ .toLowerCase();
88
+ return {
89
+ key,
90
+ raw: raw || '(unset)',
91
+ bool: raw === 'true' || raw === '1' || raw === 'yes' || raw === 'on',
92
+ isSet: raw.length > 0,
93
+ };
94
+ });
95
+
96
+ const allFalse = values.every((item) => item.isSet && item.bool === false);
97
+ const allTrue = values.every((item) => item.isSet && item.bool === true);
98
+ const mode = allFalse ? 'deterministic_on' : allTrue ? 'ai_on' : 'custom';
99
+
100
+ return {
101
+ mode,
102
+ values,
103
+ };
104
+ };
105
+
106
+ const restartPm2Process = ({ pm2Name, modeValue }) => {
107
+ const envOverrides = Object.fromEntries(CORE_AI_FLAGS.map((key) => [key, modeValue]));
108
+ const result = spawnSync('pm2', ['restart', pm2Name, '--update-env'], {
109
+ env: {
110
+ ...process.env,
111
+ ...envOverrides,
112
+ },
113
+ encoding: 'utf8',
114
+ });
115
+
116
+ if (result.error) {
117
+ throw result.error;
118
+ }
119
+ if (result.status !== 0) {
120
+ throw new Error((result.stderr || result.stdout || `Falha ao reiniciar processo PM2: ${pm2Name}`).trim());
121
+ }
122
+ };
123
+
124
+ const printStatus = ({ mode, values }) => {
125
+ console.log(`core_ai_mode=${mode}`);
126
+ for (const item of values) {
127
+ console.log(`${item.key}=${item.raw}`);
128
+ }
129
+ };
130
+
131
+ const run = async () => {
132
+ const { mode, restart, pm2Name } = parseArgs(process.argv.slice(2));
133
+
134
+ if (!['on', 'off', 'status'].includes(mode)) {
135
+ console.error('Uso: npm run core:ai -- <on|off|status> [--no-restart] [--pm2-name=<processo>]');
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+
140
+ if (mode === 'status') {
141
+ const status = await computeStatus();
142
+ printStatus(status);
143
+ return;
144
+ }
145
+
146
+ const modeValue = mode === 'on' ? 'false' : 'true';
147
+ await writeModeToEnv(modeValue);
148
+
149
+ if (restart) {
150
+ restartPm2Process({ pm2Name, modeValue });
151
+ }
152
+
153
+ const status = await computeStatus();
154
+ console.log(`core_ai_mode_updated=${mode}`);
155
+ console.log(`pm2_restart=${restart ? 'executed' : 'skipped'}`);
156
+ console.log(`pm2_process=${pm2Name}`);
157
+ printStatus(status);
158
+ };
159
+
160
+ run().catch((error) => {
161
+ console.error(`Erro ao alternar modo do core AI: ${error?.message || error}`);
162
+ process.exitCode = 1;
163
+ });
package/scripts/deploy.sh CHANGED
@@ -8,9 +8,12 @@ BACKUP_ENABLED="${DEPLOY_CREATE_BACKUP:-1}"
8
8
  BACKUP_DIR="${DEPLOY_BACKUP_DIR:-$DEPLOY_DIR/.backup}"
9
9
  NGINX_SERVICE="${DEPLOY_NGINX_SERVICE:-nginx}"
10
10
  RESTART_PM2="${DEPLOY_RESTART_PM2:-1}"
11
- PM2_APP_NAME="${DEPLOY_PM2_APP_NAME:-omnizap-system-production}"
11
+ PM2_APP_NAME="${DEPLOY_PM2_APP_NAME:-omnizap-production}"
12
12
  BUILD_ID="${DEPLOY_BUILD_ID:-$(date -u +%Y%m%d%H%M%S)}"
13
13
  VERIFY_URL="${DEPLOY_VERIFY_URL:-https://omnizap.shop/}"
14
+ WEB_SURFACE_VERIFY_ENABLED="${DEPLOY_VERIFY_WEB_SECURITY_SURFACE:-1}"
15
+ WEB_SURFACE_VERIFY_BASE_URL="${DEPLOY_VERIFY_WEB_SECURITY_SURFACE_BASE_URL:-$VERIFY_URL}"
16
+ WEB_SURFACE_VERIFY_REPORT_PATH="${DEPLOY_VERIFY_WEB_SECURITY_SURFACE_REPORT_PATH:-$PROJECT_ROOT/temp/security-web-surface-report.json}"
14
17
  DRY_RUN="${DEPLOY_DRY_RUN:-0}"
15
18
  GITHUB_NOTIFY="${DEPLOY_GITHUB_NOTIFY:-1}"
16
19
  GITHUB_ENVIRONMENT="${DEPLOY_GITHUB_ENVIRONMENT:-production}"
@@ -920,4 +923,11 @@ if command -v curl >/dev/null 2>&1; then
920
923
  fi
921
924
  fi
922
925
 
926
+ if [ "$WEB_SURFACE_VERIFY_ENABLED" = "1" ]; then
927
+ log "Executando validacao web de seguranca em $WEB_SURFACE_VERIFY_BASE_URL"
928
+ SECURITY_WEB_SURFACE_BASE_URL="$WEB_SURFACE_VERIFY_BASE_URL" \
929
+ SECURITY_WEB_SURFACE_REPORT_PATH="$WEB_SURFACE_VERIFY_REPORT_PATH" \
930
+ node "$PROJECT_ROOT/scripts/security-web-surface-check.mjs"
931
+ fi
932
+
923
933
  log "Deploy concluído com sucesso."
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
3
  import 'dotenv/config';
3
4
 
4
5
  import { closePool, executeQuery, TABLES } from '../database/index.js';
@@ -53,7 +54,7 @@ const normalizeTag = (value, fallback) =>
53
54
  .replace(/[^a-z0-9:_-]/g, '')
54
55
  .slice(0, 60);
55
56
 
56
- const nowIsoDate = new Date().toISOString().slice(0, 10);
57
+ const nowIsoDate = __timeNowIso().slice(0, 10);
57
58
  const args = parseCliArgs(process.argv.slice(2));
58
59
 
59
60
  const dryRun = parseBoolArg(args.get('--dry-run'), false);
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import OpenAI from 'openai';
9
+ import prettier from 'prettier';
10
+ import { z } from 'zod';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const repoRoot = path.resolve(__dirname, '..');
15
+ const modulesRoot = path.join(repoRoot, 'app', 'modules');
16
+
17
+ const DEFAULT_MODEL = String(process.env.COMMAND_CONFIG_UX_ENRICH_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini').trim() || 'gpt-4o-mini';
18
+ const DEFAULT_DELAY_MS = Math.max(0, Number.parseInt(String(process.env.COMMAND_CONFIG_UX_ENRICH_DELAY_MS || '120'), 10) || 120);
19
+ const MAX_ATTEMPTS = Math.max(1, Number.parseInt(String(process.env.COMMAND_CONFIG_UX_ENRICH_MAX_ATTEMPTS || '3'), 10) || 3);
20
+
21
+ const UX_FIELDS = ['resumo_usuario', 'quando_usar', 'exemplos_reais', 'resposta_esperada', 'erros_comuns_usuario', 'passos_se_der_erro'];
22
+
23
+ const OUTPUT_SCHEMA = z
24
+ .object({
25
+ resumo_usuario: z.string(),
26
+ quando_usar: z.array(z.string()),
27
+ exemplos_reais: z.array(
28
+ z.object({
29
+ situacao: z.string(),
30
+ comando: z.string(),
31
+ resposta_esperada: z.string(),
32
+ variacao: z.string().optional(),
33
+ }),
34
+ ),
35
+ resposta_esperada: z.array(z.string()),
36
+ erros_comuns_usuario: z.array(z.string()),
37
+ passos_se_der_erro: z.array(z.string()),
38
+ })
39
+ .strict();
40
+
41
+ const SYSTEM_PROMPT = ['Voce escreve textos de ajuda para usuario final de um bot WhatsApp.', 'Responda SOMENTE JSON valido com as chaves exatas:', '{"resumo_usuario":"","quando_usar":[],"exemplos_reais":[{"situacao":"","comando":"","resposta_esperada":"","variacao":""}],"resposta_esperada":[],"erros_comuns_usuario":[],"passos_se_der_erro":[]}.', 'Regras:', '- pt-BR simples, objetivo e pratico.', '- Nao use linguagem tecnica de desenvolvimento.', '- Mostre acao concreta do usuario (o que fazer agora).', '- Comandos em exemplos devem manter "<prefix>" quando aplicavel.', '- Inclua restricao Premium quando existir.', '- Evite promessas absolutas e evite texto repetido.'].join(' ');
42
+
43
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
44
+
45
+ const isObject = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
46
+
47
+ const ensureArray = (value) => (Array.isArray(value) ? value : []);
48
+
49
+ const normalizeText = (value) =>
50
+ String(value || '')
51
+ .trim()
52
+ .replace(/\s+/g, ' ');
53
+
54
+ const ensureSentence = (value) => {
55
+ const text = normalizeText(value);
56
+ if (!text) return '';
57
+ if (/[.!?]$/.test(text)) return text;
58
+ return `${text}.`;
59
+ };
60
+
61
+ const uniqueStrings = (values, { max = 8, minLength = 2, maxLength = 220 } = {}) => {
62
+ const out = [];
63
+ const seen = new Set();
64
+ for (const value of ensureArray(values)) {
65
+ const normalized = normalizeText(value).slice(0, maxLength);
66
+ if (!normalized || normalized.length < minLength) continue;
67
+ const key = normalized.toLowerCase();
68
+ if (seen.has(key)) continue;
69
+ seen.add(key);
70
+ out.push(normalized);
71
+ if (out.length >= max) break;
72
+ }
73
+ return out;
74
+ };
75
+
76
+ const parseArgs = (argv) => {
77
+ const options = {
78
+ moduleFilter: '',
79
+ commandFilter: '',
80
+ limit: Number.POSITIVE_INFINITY,
81
+ overwrite: false,
82
+ dryRun: false,
83
+ model: DEFAULT_MODEL,
84
+ delayMs: DEFAULT_DELAY_MS,
85
+ };
86
+
87
+ for (const arg of argv) {
88
+ if (!arg) continue;
89
+ if (arg === '--overwrite') {
90
+ options.overwrite = true;
91
+ continue;
92
+ }
93
+ if (arg === '--dry-run') {
94
+ options.dryRun = true;
95
+ continue;
96
+ }
97
+ if (arg.startsWith('--module=')) {
98
+ options.moduleFilter = normalizeText(arg.slice('--module='.length)).toLowerCase();
99
+ continue;
100
+ }
101
+ if (arg.startsWith('--command=')) {
102
+ options.commandFilter = normalizeText(arg.slice('--command='.length)).toLowerCase();
103
+ continue;
104
+ }
105
+ if (arg.startsWith('--limit=')) {
106
+ const parsed = Number.parseInt(arg.slice('--limit='.length), 10);
107
+ if (Number.isFinite(parsed) && parsed > 0) {
108
+ options.limit = parsed;
109
+ }
110
+ continue;
111
+ }
112
+ if (arg.startsWith('--model=')) {
113
+ const model = normalizeText(arg.slice('--model='.length));
114
+ if (model) options.model = model;
115
+ continue;
116
+ }
117
+ if (arg.startsWith('--delay-ms=')) {
118
+ const parsed = Number.parseInt(arg.slice('--delay-ms='.length), 10);
119
+ if (Number.isFinite(parsed) && parsed >= 0) {
120
+ options.delayMs = parsed;
121
+ }
122
+ }
123
+ }
124
+
125
+ return options;
126
+ };
127
+
128
+ const listModuleConfigPaths = async () => {
129
+ const dirs = await fs.readdir(modulesRoot, { withFileTypes: true });
130
+ const files = [];
131
+ for (const entry of dirs) {
132
+ if (!entry.isDirectory()) continue;
133
+ const configPath = path.join(modulesRoot, entry.name, 'commandConfig.json');
134
+ try {
135
+ await fs.access(configPath);
136
+ files.push(configPath);
137
+ } catch {
138
+ // ignore modules sem commandConfig
139
+ }
140
+ }
141
+ return files.sort((a, b) => a.localeCompare(b, 'pt-BR'));
142
+ };
143
+
144
+ const commandMatchesFilter = (command, commandFilter) => {
145
+ if (!commandFilter) return true;
146
+ const name = normalizeText(command?.name).toLowerCase();
147
+ if (name.includes(commandFilter)) return true;
148
+ const aliases = ensureArray(command?.aliases).map((alias) => normalizeText(alias).toLowerCase());
149
+ return aliases.some((alias) => alias.includes(commandFilter));
150
+ };
151
+
152
+ const resolveRequirements = (command) => {
153
+ const req = isObject(command?.requirements) ? command.requirements : isObject(command?.pre_condicoes) ? command.pre_condicoes : {};
154
+ return {
155
+ group: Boolean(req.require_group ?? req.requer_grupo),
156
+ admin: Boolean(req.require_group_admin ?? req.requer_admin),
157
+ owner: Boolean(req.require_bot_owner ?? req.requer_admin_principal),
158
+ googleLogin: Boolean(req.require_google_login ?? req.requer_google_login),
159
+ nsfw: Boolean(req.require_nsfw_enabled ?? req.requer_nsfw),
160
+ media: Boolean(req.require_media ?? req.requer_midia),
161
+ reply: Boolean(req.require_reply_message ?? req.requer_mensagem_respondida),
162
+ };
163
+ };
164
+
165
+ const resolvePremium = (command) => {
166
+ const access = isObject(command?.access) ? command.access : isObject(command?.acesso) ? command.acesso : {};
167
+ return {
168
+ premium: Boolean(access.premium_only ?? access.somente_premium),
169
+ plans: uniqueStrings(access.allowed_plans || access.planos_permitidos, { max: 8, maxLength: 40 }),
170
+ };
171
+ };
172
+
173
+ const resolveUsageMethods = (command) =>
174
+ uniqueStrings(command?.metodos_de_uso || command?.usage, {
175
+ max: 6,
176
+ maxLength: 220,
177
+ });
178
+
179
+ const resolveResponses = (command) => {
180
+ const responses = isObject(command?.responses) ? command.responses : isObject(command?.respostas_padrao) ? command.respostas_padrao : {};
181
+ return {
182
+ success: normalizeText(responses.success || responses.sucesso),
183
+ usageError: normalizeText(responses.usage_error || responses.erro_uso),
184
+ permissionError: normalizeText(responses.permission_error || responses.erro_permissao),
185
+ };
186
+ };
187
+
188
+ const resolveArgs = (command) =>
189
+ ensureArray(command?.arguments || command?.argumentos)
190
+ .map((arg) => ({
191
+ name: normalizeText(arg?.name || arg?.nome),
192
+ type: normalizeText(arg?.type || arg?.tipo || 'string'),
193
+ required: Boolean(arg?.required ?? arg?.obrigatorio),
194
+ description: normalizeText(arg?.description || arg?.descricao),
195
+ }))
196
+ .filter((arg) => arg.name)
197
+ .slice(0, 6);
198
+
199
+ const normalizeExampleCommand = (value, fallback) => {
200
+ const raw = normalizeText(value);
201
+ const candidate = raw || normalizeText(fallback);
202
+ if (!candidate) return '';
203
+ if (candidate.startsWith('/')) return `<prefix>${candidate.slice(1)}`;
204
+ return candidate;
205
+ };
206
+
207
+ const buildFallbackUx = (context) => {
208
+ const commandName = context?.name || 'comando';
209
+ const commandUsage = context?.usageMethods?.[0] || `<prefix>${commandName}`;
210
+ const description = normalizeText(context?.description) || `Use <prefix>${commandName} para executar esta acao`;
211
+ const successText = normalizeText(context?.responses?.success) || 'O bot confirma que executou com sucesso';
212
+ const usageErrorText = normalizeText(context?.responses?.usageError) || 'Se o formato estiver errado, o bot mostra como corrigir';
213
+ const permissionText = normalizeText(context?.responses?.permissionError) || 'Sem permissao, o bot informa o motivo';
214
+
215
+ return {
216
+ resumo_usuario: ensureSentence(description),
217
+ quando_usar: uniqueStrings([`Quando voce precisa desta acao: ${ensureSentence(description)}`, context?.requirements?.group ? 'Funciona dentro de grupos.' : 'Pode ser usado no privado e em grupo.', context?.requirements?.admin ? 'Voce precisa ser admin para executar.' : '', context?.premium?.premium ? 'Disponivel para usuarios Premium.' : ''], { max: 5 }),
218
+ exemplos_reais: [
219
+ {
220
+ situacao: ensureSentence(`Cenario comum para usar ${commandUsage}`),
221
+ comando: normalizeExampleCommand(commandUsage, `<prefix>${commandName}`),
222
+ resposta_esperada: ensureSentence(successText),
223
+ variacao: ensureSentence(usageErrorText),
224
+ },
225
+ ],
226
+ resposta_esperada: uniqueStrings([`Sucesso: ${ensureSentence(successText)}`, `Uso incorreto: ${ensureSentence(usageErrorText)}`, `Permissao: ${ensureSentence(permissionText)}`], { max: 5 }),
227
+ erros_comuns_usuario: uniqueStrings(['Digitar o comando fora do formato esperado.', context?.requirements?.group ? 'Tentar executar fora de um grupo.' : '', context?.requirements?.admin ? 'Tentar executar sem ser admin.' : '', context?.premium?.premium ? 'Tentar usar sem plano Premium ativo.' : ''], { max: 5 }),
228
+ passos_se_der_erro: uniqueStrings(['Copie e teste um exemplo desta pagina.', 'Confira se voce esta no local correto e com permissao.', 'Se continuar com erro, fale com o admin no privado.'], { max: 5 }),
229
+ };
230
+ };
231
+
232
+ const sanitizeUxPayload = (payload, context) => {
233
+ const fallback = buildFallbackUx(context);
234
+ const usageFallback = context?.usageMethods?.[0] || `<prefix>${context?.name || 'comando'}`;
235
+
236
+ const examples = ensureArray(payload?.exemplos_reais)
237
+ .map((example) => {
238
+ if (!isObject(example)) return null;
239
+ const situacao = ensureSentence(example.situacao) || ensureSentence(fallback.exemplos_reais[0].situacao);
240
+ const comando = normalizeExampleCommand(example.comando, usageFallback);
241
+ const resposta = ensureSentence(example.resposta_esperada) || ensureSentence(fallback.exemplos_reais[0].resposta_esperada);
242
+ const variacao = ensureSentence(example.variacao || '') || ensureSentence(fallback.exemplos_reais[0].variacao);
243
+ if (!comando) return null;
244
+ return {
245
+ situacao,
246
+ comando,
247
+ resposta_esperada: resposta,
248
+ variacao,
249
+ };
250
+ })
251
+ .filter(Boolean)
252
+ .slice(0, 3);
253
+
254
+ const normalized = {
255
+ resumo_usuario: ensureSentence(payload?.resumo_usuario) || fallback.resumo_usuario,
256
+ quando_usar: uniqueStrings(payload?.quando_usar, { max: 5 }),
257
+ exemplos_reais: examples,
258
+ resposta_esperada: uniqueStrings(payload?.resposta_esperada, { max: 5 }),
259
+ erros_comuns_usuario: uniqueStrings(payload?.erros_comuns_usuario, { max: 5 }),
260
+ passos_se_der_erro: uniqueStrings(payload?.passos_se_der_erro, { max: 5 }),
261
+ };
262
+
263
+ if (!normalized.quando_usar.length) normalized.quando_usar = fallback.quando_usar;
264
+ if (!normalized.exemplos_reais.length) normalized.exemplos_reais = fallback.exemplos_reais;
265
+ if (!normalized.resposta_esperada.length) normalized.resposta_esperada = fallback.resposta_esperada;
266
+ if (!normalized.erros_comuns_usuario.length) normalized.erros_comuns_usuario = fallback.erros_comuns_usuario;
267
+ if (!normalized.passos_se_der_erro.length) normalized.passos_se_der_erro = fallback.passos_se_der_erro;
268
+
269
+ return normalized;
270
+ };
271
+
272
+ const extractCurrentUx = (command) => {
273
+ const userExperience = isObject(command?.user_experience) ? command.user_experience : {};
274
+ return {
275
+ resumo_usuario: userExperience.resumo_usuario ?? command?.resumo_usuario ?? '',
276
+ quando_usar: userExperience.quando_usar ?? command?.quando_usar ?? [],
277
+ exemplos_reais: userExperience.exemplos_reais ?? command?.exemplos_reais ?? [],
278
+ resposta_esperada: userExperience.resposta_esperada ?? command?.resposta_esperada ?? [],
279
+ erros_comuns_usuario: userExperience.erros_comuns_usuario ?? command?.erros_comuns_usuario ?? [],
280
+ passos_se_der_erro: userExperience.passos_se_der_erro ?? command?.passos_se_der_erro ?? [],
281
+ };
282
+ };
283
+
284
+ const hasCompleteUx = (command) => {
285
+ const ux = extractCurrentUx(command);
286
+ return normalizeText(ux.resumo_usuario).length >= 8 && uniqueStrings(ux.quando_usar, { max: 10 }).length > 0 && ensureArray(ux.exemplos_reais).length > 0 && uniqueStrings(ux.resposta_esperada, { max: 10 }).length > 0 && uniqueStrings(ux.erros_comuns_usuario, { max: 10 }).length > 0 && uniqueStrings(ux.passos_se_der_erro, { max: 10 }).length > 0;
287
+ };
288
+
289
+ const buildCommandContext = ({ moduleDirName, command }) => {
290
+ const name = normalizeText(command?.name);
291
+ const description = normalizeText(command?.description || command?.descricao);
292
+ const usageMethods = resolveUsageMethods(command);
293
+ const requirements = resolveRequirements(command);
294
+ const premium = resolvePremium(command);
295
+ const responses = resolveResponses(command);
296
+ const argumentsList = resolveArgs(command);
297
+ const aliases = uniqueStrings(command?.aliases, { max: 8, maxLength: 60 });
298
+ const category = normalizeText(command?.categoria || command?.category);
299
+ const currentUx = extractCurrentUx(command);
300
+
301
+ return {
302
+ module: moduleDirName,
303
+ name,
304
+ aliases,
305
+ category,
306
+ description,
307
+ usageMethods,
308
+ requirements,
309
+ premium,
310
+ responses,
311
+ arguments: argumentsList,
312
+ currentUx,
313
+ };
314
+ };
315
+
316
+ const extractJsonContent = (completion) => {
317
+ const content = completion?.choices?.[0]?.message?.content;
318
+ if (typeof content === 'string') return content;
319
+ if (Array.isArray(content)) {
320
+ return content
321
+ .map((item) => {
322
+ if (!item) return '';
323
+ if (typeof item === 'string') return item;
324
+ if (typeof item?.text === 'string') return item.text;
325
+ if (typeof item?.text?.value === 'string') return item.text.value;
326
+ return '';
327
+ })
328
+ .filter(Boolean)
329
+ .join('\n')
330
+ .trim();
331
+ }
332
+ return '';
333
+ };
334
+
335
+ const generateUxWithOpenAI = async ({ client, model, context }) => {
336
+ const userPayload = {
337
+ objective: 'Gerar conteudo de pagina de comando focado em usuario final',
338
+ output_keys: UX_FIELDS,
339
+ command_context: context,
340
+ };
341
+
342
+ let lastError = null;
343
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
344
+ try {
345
+ const requestPayload = {
346
+ model,
347
+ response_format: { type: 'json_object' },
348
+ messages: [
349
+ { role: 'system', content: SYSTEM_PROMPT },
350
+ { role: 'user', content: JSON.stringify(userPayload, null, 2) },
351
+ ],
352
+ };
353
+ if (
354
+ !String(model || '')
355
+ .trim()
356
+ .toLowerCase()
357
+ .startsWith('gpt-5')
358
+ ) {
359
+ requestPayload.temperature = 0.2;
360
+ }
361
+
362
+ const completion = await client.chat.completions.create(requestPayload);
363
+
364
+ const content = extractJsonContent(completion);
365
+ const parsed = JSON.parse(content);
366
+ const validated = OUTPUT_SCHEMA.parse(parsed);
367
+ return sanitizeUxPayload(validated, context);
368
+ } catch (error) {
369
+ lastError = error;
370
+ if (attempt < MAX_ATTEMPTS) {
371
+ await sleep(400 * attempt);
372
+ }
373
+ }
374
+ }
375
+
376
+ throw lastError || new Error('Falha ao gerar UX com OpenAI');
377
+ };
378
+
379
+ const writeFormattedJson = async (filePath, payload) => {
380
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
381
+ const prettierConfig = (await prettier.resolveConfig(filePath)) || {};
382
+ const formatted = await prettier.format(serialized, {
383
+ ...prettierConfig,
384
+ parser: 'json',
385
+ filepath: filePath,
386
+ });
387
+ await fs.writeFile(filePath, formatted, 'utf8');
388
+ };
389
+
390
+ const main = async () => {
391
+ const options = parseArgs(process.argv.slice(2));
392
+ const apiKey = normalizeText(process.env.OPENAI_API_KEY);
393
+ if (!apiKey) {
394
+ throw new Error('OPENAI_API_KEY nao configurada. Defina no ambiente ou no arquivo .env');
395
+ }
396
+
397
+ const client = new OpenAI({
398
+ apiKey,
399
+ timeout: 30_000,
400
+ maxRetries: 1,
401
+ });
402
+
403
+ const configPaths = await listModuleConfigPaths();
404
+ const stats = {
405
+ scanned: 0,
406
+ target: 0,
407
+ updated: 0,
408
+ skippedExisting: 0,
409
+ failed: 0,
410
+ filesChanged: 0,
411
+ };
412
+
413
+ console.log(`[ux-enrich] model=${options.model} dryRun=${options.dryRun} overwrite=${options.overwrite} limit=${Number.isFinite(options.limit) ? options.limit : 'all'} delayMs=${options.delayMs}`);
414
+
415
+ for (const configPath of configPaths) {
416
+ const moduleDirName = path.basename(path.dirname(configPath));
417
+ if (options.moduleFilter && moduleDirName.toLowerCase() !== options.moduleFilter) {
418
+ continue;
419
+ }
420
+
421
+ const raw = await fs.readFile(configPath, 'utf8');
422
+ const parsed = JSON.parse(raw);
423
+ const commands = ensureArray(parsed?.commands);
424
+
425
+ let fileChanged = false;
426
+
427
+ for (const command of commands) {
428
+ if (stats.target >= options.limit) break;
429
+ if (!isObject(command)) continue;
430
+ if (command.enabled === false) continue;
431
+ if (!commandMatchesFilter(command, options.commandFilter)) continue;
432
+
433
+ stats.scanned += 1;
434
+
435
+ if (!options.overwrite && hasCompleteUx(command)) {
436
+ stats.skippedExisting += 1;
437
+ continue;
438
+ }
439
+
440
+ const commandName = normalizeText(command.name);
441
+ if (!commandName) continue;
442
+
443
+ stats.target += 1;
444
+ const context = buildCommandContext({ moduleDirName, command });
445
+
446
+ try {
447
+ const ux = await generateUxWithOpenAI({
448
+ client,
449
+ model: options.model,
450
+ context,
451
+ });
452
+
453
+ const previousUx = isObject(command.user_experience) ? command.user_experience : {};
454
+ command.user_experience = {
455
+ ...previousUx,
456
+ ...ux,
457
+ resumo_usuario_origem: 'auto_ia_assistida',
458
+ resumo_usuario_revisao_pendente: true,
459
+ };
460
+
461
+ stats.updated += 1;
462
+ fileChanged = true;
463
+ console.log(`[ux-enrich] ok ${moduleDirName}/${commandName}`);
464
+ } catch (error) {
465
+ stats.failed += 1;
466
+ console.warn(`[ux-enrich] fail ${moduleDirName}/${commandName}: ${error?.message || 'erro desconhecido'}`);
467
+ }
468
+
469
+ if (options.delayMs > 0) {
470
+ await sleep(options.delayMs);
471
+ }
472
+ }
473
+
474
+ if (fileChanged) {
475
+ stats.filesChanged += 1;
476
+ if (!options.dryRun) {
477
+ await writeFormattedJson(configPath, parsed);
478
+ }
479
+ console.log(`[ux-enrich] file ${options.dryRun ? 'would-update' : 'updated'} ${path.relative(repoRoot, configPath)}`);
480
+ }
481
+
482
+ if (stats.target >= options.limit) break;
483
+ }
484
+
485
+ console.log('[ux-enrich] done');
486
+ console.log([`scanned=${stats.scanned}`, `target=${stats.target}`, `updated=${stats.updated}`, `skipped_existing=${stats.skippedExisting}`, `failed=${stats.failed}`, `files_changed=${stats.filesChanged}`].join(' '));
487
+ };
488
+
489
+ main().catch((error) => {
490
+ console.error(`[ux-enrich] fatal: ${error?.message || error}`);
491
+ process.exitCode = 1;
492
+ });