@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
@@ -4,9 +4,13 @@ import path from 'node:path';
4
4
  import OpenAI from 'openai';
5
5
  import { z } from 'zod';
6
6
 
7
+ import { toUnixMs as __timeNowMs } from '#time';
7
8
  import logger from '#logger';
9
+ import { createGeminiTextService, DEFAULT_GEMINI_MODEL, isGeminiAuthReady } from './geminiService.js';
8
10
 
9
- const DEFAULT_MODEL = 'gpt-4o-mini';
11
+ const DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
12
+ const DEFAULT_PROVIDER = 'gemini';
13
+ const DEFAULT_GEMINI_AUTH_MODE = 'cli';
10
14
  const DEFAULT_TIMEOUT_MS = 25_000;
11
15
  const DEFAULT_CONTEXT_MAX_CHARS = 5_200;
12
16
  const DEFAULT_AGENT_MAX_CHARS = 2_400;
@@ -28,13 +32,52 @@ const parseEnvFloat = (value, fallback, min, max) => {
28
32
  return Math.max(min, Math.min(max, parsed));
29
33
  };
30
34
 
35
+ const normalizeProvider = (value, fallback = DEFAULT_PROVIDER) => {
36
+ const normalized = String(value || '')
37
+ .trim()
38
+ .toLowerCase();
39
+ if (normalized === 'gemini') return 'gemini';
40
+ if (normalized === 'openai') return 'openai';
41
+ return fallback;
42
+ };
43
+
44
+ const normalizeGeminiAuthMode = (value, fallback = DEFAULT_GEMINI_AUTH_MODE) => {
45
+ const normalized = String(value || '')
46
+ .trim()
47
+ .toLowerCase();
48
+ if (normalized === 'api_key') return 'api_key';
49
+ if (normalized === 'cli') return 'cli';
50
+ if (normalized === 'auto') return 'auto';
51
+ return fallback;
52
+ };
53
+
54
+ const looksLikeGeminiModel = (value) =>
55
+ String(value || '')
56
+ .trim()
57
+ .toLowerCase()
58
+ .includes('gemini');
59
+
60
+ const looksLikeOpenAiModel = (value) => {
61
+ const normalized = String(value || '')
62
+ .trim()
63
+ .toLowerCase();
64
+ if (!normalized) return false;
65
+ return normalized.startsWith('gpt-') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4') || normalized.startsWith('text-');
66
+ };
67
+
68
+ const COMMAND_CONFIG_ENRICHMENT_PROVIDER = normalizeProvider(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_PROVIDER || process.env.AI_HELP_LLM_PROVIDER || DEFAULT_PROVIDER, DEFAULT_PROVIDER);
31
69
  const COMMAND_CONFIG_ENRICHMENT_MODEL = String(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_MODEL || DEFAULT_MODEL).trim() || DEFAULT_MODEL;
70
+ const COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL = looksLikeGeminiModel(COMMAND_CONFIG_ENRICHMENT_MODEL) ? COMMAND_CONFIG_ENRICHMENT_MODEL : String(process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL;
71
+ const COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL = looksLikeOpenAiModel(COMMAND_CONFIG_ENRICHMENT_MODEL) ? COMMAND_CONFIG_ENRICHMENT_MODEL : String(process.env.OPENAI_MODEL || 'gpt-5-nano').trim() || 'gpt-5-nano';
32
72
  const COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 5_000, 90_000);
33
73
  const COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS, DEFAULT_CONTEXT_MAX_CHARS, 1_500, 16_000);
34
74
  const COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS, DEFAULT_AGENT_MAX_CHARS, 600, 8_000);
35
75
  const COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS, DEFAULT_SOURCE_FILE_MAX_CHARS, 400, 5_000);
36
76
  const COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES, DEFAULT_MAX_SOURCE_FILES, 1, 8);
37
77
  const COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE = parseEnvFloat(process.env.COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE, 0.55, 0.1, 1);
78
+ const COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS, 15 * 60 * 1000, 30_000, 24 * 60 * 60 * 1000);
79
+ const COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE = normalizeGeminiAuthMode(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_GEMINI_AUTH_MODE || process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, DEFAULT_GEMINI_AUTH_MODE);
80
+ const COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND = String(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_GEMINI_CLI_COMMAND || process.env.GEMINI_CLI_COMMAND || 'gemini').trim() || 'gemini';
38
81
 
39
82
  const AI_ENRICHMENT_OUTPUT_SCHEMA = z
40
83
  .object({
@@ -48,6 +91,12 @@ const AI_ENRICHMENT_OUTPUT_SCHEMA = z
48
91
  .strict();
49
92
 
50
93
  let cachedClient = null;
94
+ let cachedGeminiService = null;
95
+ let cachedGeminiServiceKey = '';
96
+ const providerCooldownUntil = {
97
+ gemini: 0,
98
+ openai: 0,
99
+ };
51
100
 
52
101
  const moduleConfigCache = new Map();
53
102
  const fileTextCache = new Map();
@@ -105,6 +154,51 @@ const parseJsonSafe = (value) => {
105
154
  }
106
155
  };
107
156
 
157
+ const parseJsonFromModelOutput = (rawOutput) => {
158
+ const direct = parseJsonSafe(rawOutput);
159
+ if (direct && typeof direct === 'object') return direct;
160
+
161
+ const text = String(rawOutput || '').trim();
162
+ if (!text) return null;
163
+
164
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
165
+ if (fenceMatch?.[1]) {
166
+ const fenced = parseJsonSafe(fenceMatch[1]);
167
+ if (fenced && typeof fenced === 'object') return fenced;
168
+ }
169
+
170
+ const start = text.indexOf('{');
171
+ const end = text.lastIndexOf('}');
172
+ if (start >= 0 && end > start) {
173
+ const sliced = parseJsonSafe(text.slice(start, end + 1));
174
+ if (sliced && typeof sliced === 'object') return sliced;
175
+ }
176
+
177
+ return null;
178
+ };
179
+
180
+ const isProviderInCooldown = (provider) => {
181
+ const now = __timeNowMs();
182
+ const safeProvider = provider === 'openai' ? 'openai' : 'gemini';
183
+ return Number(providerCooldownUntil[safeProvider] || 0) > now;
184
+ };
185
+
186
+ const shouldApplyProviderCooldown = (provider, error) => {
187
+ const message = normalizeText(error?.message || error);
188
+ if (!message) return false;
189
+
190
+ const hardErrorPatterns = ['modelnotfound', 'does not exist', 'requested entity was not found', 'permission denied', 'unauthorized', 'invalid api key', 'quota', '429'];
191
+ if (hardErrorPatterns.some((token) => message.includes(token))) return true;
192
+ return false;
193
+ };
194
+
195
+ const markProviderCooldown = (provider, error) => {
196
+ if (!shouldApplyProviderCooldown(provider, error)) return false;
197
+ const safeProvider = provider === 'openai' ? 'openai' : 'gemini';
198
+ providerCooldownUntil[safeProvider] = __timeNowMs() + COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS;
199
+ return true;
200
+ };
201
+
108
202
  const uniqueList = (values = [], { maxItems = DEFAULT_MAX_LIST_ITEMS, maxLength = 200, normalizeMode = 'display' } = {}) => {
109
203
  const source = Array.isArray(values) ? values : [];
110
204
  const output = [];
@@ -175,6 +269,86 @@ const getOpenAIClient = () => {
175
269
  return cachedClient;
176
270
  };
177
271
 
272
+ const isGeminiReady = () =>
273
+ isGeminiAuthReady({
274
+ authMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
275
+ apiKey: process.env.GEMINI_API_KEY,
276
+ cliCommand: COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND,
277
+ });
278
+
279
+ const isOpenAiReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
280
+
281
+ const getGeminiService = () => {
282
+ if (!isGeminiReady()) return null;
283
+
284
+ const serviceKey = `${COMMAND_CONFIG_ENRICHMENT_MODEL}|${COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS}|${COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE}|${COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND}|${Boolean(String(process.env.GEMINI_API_KEY || '').trim())}`;
285
+ if (!cachedGeminiService || cachedGeminiServiceKey !== serviceKey) {
286
+ cachedGeminiService = createGeminiTextService({
287
+ apiKey: process.env.GEMINI_API_KEY,
288
+ defaultModel: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
289
+ timeoutMs: COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS,
290
+ authMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
291
+ cliCommand: COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND,
292
+ });
293
+ cachedGeminiServiceKey = serviceKey;
294
+ }
295
+ return cachedGeminiService;
296
+ };
297
+
298
+ const callGeminiEnrichment = async ({ systemPrompt, contextPayload }) => {
299
+ const service = getGeminiService();
300
+ if (!service) return null;
301
+
302
+ const response = await service.generateText({
303
+ instructions: systemPrompt,
304
+ userPrompt: contextPayload,
305
+ model: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
306
+ });
307
+
308
+ const text = String(response?.text || '').trim();
309
+ if (!text) return null;
310
+ return {
311
+ provider: 'gemini',
312
+ model: String(response?.model || COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL).trim() || COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
313
+ outputText: text,
314
+ };
315
+ };
316
+
317
+ const callOpenAiEnrichment = async ({ systemPrompt, contextPayload }) => {
318
+ if (!isOpenAiReady()) return null;
319
+
320
+ const client = getOpenAIClient();
321
+ const completion = await client.chat.completions.create({
322
+ model: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
323
+ response_format: { type: 'json_object' },
324
+ messages: [
325
+ {
326
+ role: 'system',
327
+ content: systemPrompt,
328
+ },
329
+ {
330
+ role: 'user',
331
+ content: contextPayload,
332
+ },
333
+ ],
334
+ });
335
+
336
+ const message = completion?.choices?.[0]?.message || {};
337
+ const outputText = extractTextFromAssistantMessage(message);
338
+ if (!outputText) return null;
339
+ return {
340
+ provider: 'openai',
341
+ model: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
342
+ outputText,
343
+ };
344
+ };
345
+
346
+ const resolveLlmCallOrder = () => {
347
+ const primary = normalizeProvider(COMMAND_CONFIG_ENRICHMENT_PROVIDER, DEFAULT_PROVIDER);
348
+ const fallback = primary === 'gemini' ? 'openai' : 'gemini';
349
+ return [primary, fallback];
350
+ };
351
+
178
352
  const isFilePathInside = (baseDir, candidatePath) => {
179
353
  const normalizedBase = path.resolve(baseDir);
180
354
  const normalizedCandidate = path.resolve(candidatePath);
@@ -333,7 +507,7 @@ const buildCommandContextPayload = async ({ learningEvent, toolRecord }) => {
333
507
  };
334
508
 
335
509
  const parseAndSanitizeOutput = (rawJson) => {
336
- const parsed = parseJsonSafe(rawJson);
510
+ const parsed = parseJsonFromModelOutput(rawJson);
337
511
  if (!parsed || typeof parsed !== 'object') return null;
338
512
 
339
513
  const validated = AI_ENRICHMENT_OUTPUT_SCHEMA.safeParse(parsed);
@@ -379,7 +553,7 @@ const buildHeuristicSuggestion = ({ learningEvent, toolRecord }) => {
379
553
  };
380
554
  };
381
555
 
382
- const isLlmReady = () => Boolean(process.env.OPENAI_API_KEY);
556
+ const isLlmReady = () => isGeminiReady() || isOpenAiReady();
383
557
 
384
558
  export const generateCommandConfigEnrichmentSuggestion = async ({ learningEvent, toolRecord } = {}) => {
385
559
  if (!learningEvent || !toolRecord) return null;
@@ -390,63 +564,71 @@ export const generateCommandConfigEnrichmentSuggestion = async ({ learningEvent,
390
564
  }
391
565
 
392
566
  const contextPayload = await buildCommandContextPayload({ learningEvent, toolRecord });
393
- const client = getOpenAIClient();
567
+ const systemPrompt = buildSystemPrompt();
394
568
 
395
- let completion = null;
396
- try {
397
- completion = await client.chat.completions.create({
398
- model: COMMAND_CONFIG_ENRICHMENT_MODEL,
399
- temperature: 0.15,
400
- response_format: { type: 'json_object' },
401
- messages: [
402
- {
403
- role: 'system',
404
- content: buildSystemPrompt(),
405
- },
406
- {
407
- role: 'user',
408
- content: contextPayload,
409
- },
410
- ],
411
- });
412
- } catch (error) {
413
- logger.warn('Falha ao gerar enriquecimento de commandConfig com LLM.', {
414
- action: 'command_config_enrichment_llm_failed',
415
- module: toolRecord?.moduleKey || null,
416
- command: toolRecord?.commandName || null,
417
- event_id: learningEvent?.id || null,
418
- error: error?.message,
419
- });
420
- return fallbackSuggestion;
421
- }
422
-
423
- const message = completion?.choices?.[0]?.message || {};
424
- const rawJson = extractTextFromAssistantMessage(message);
425
- const parsed = parseAndSanitizeOutput(rawJson);
569
+ const providerOrder = resolveLlmCallOrder();
570
+ for (const provider of providerOrder) {
571
+ if (isProviderInCooldown(provider)) {
572
+ continue;
573
+ }
426
574
 
427
- if (!parsed || !isSuggestionMeaningful(parsed.suggestion)) {
428
- return fallbackSuggestion;
575
+ try {
576
+ const llmOutput =
577
+ provider === 'gemini'
578
+ ? await callGeminiEnrichment({
579
+ systemPrompt,
580
+ contextPayload,
581
+ })
582
+ : await callOpenAiEnrichment({
583
+ systemPrompt,
584
+ contextPayload,
585
+ });
586
+
587
+ if (!llmOutput?.outputText) continue;
588
+
589
+ const parsed = parseAndSanitizeOutput(llmOutput.outputText);
590
+ if (!parsed || !isSuggestionMeaningful(parsed.suggestion)) continue;
591
+
592
+ const eventConfidence = clamp01(learningEvent?.confidence);
593
+ const successSignal = learningEvent?.success ? 0.12 : 0.03;
594
+ const finalConfidence = clamp01(parsed.modelConfidence * 0.72 + eventConfidence * 0.18 + successSignal);
595
+
596
+ return {
597
+ suggestion: parsed.suggestion,
598
+ confidence: finalConfidence,
599
+ source: `llm_${provider}`,
600
+ modelName: llmOutput.model || COMMAND_CONFIG_ENRICHMENT_MODEL,
601
+ };
602
+ } catch (error) {
603
+ logger.warn('Falha ao gerar enriquecimento de commandConfig com LLM.', {
604
+ action: 'command_config_enrichment_llm_failed',
605
+ module: toolRecord?.moduleKey || null,
606
+ command: toolRecord?.commandName || null,
607
+ event_id: learningEvent?.id || null,
608
+ provider,
609
+ provider_cooldown_applied: markProviderCooldown(provider, error),
610
+ error: error?.message,
611
+ });
612
+ }
429
613
  }
430
614
 
431
- const eventConfidence = clamp01(learningEvent?.confidence);
432
- const successSignal = learningEvent?.success ? 0.12 : 0.03;
433
- const finalConfidence = clamp01(parsed.modelConfidence * 0.72 + eventConfidence * 0.18 + successSignal);
434
-
435
- return {
436
- suggestion: parsed.suggestion,
437
- confidence: finalConfidence,
438
- source: 'llm',
439
- modelName: COMMAND_CONFIG_ENRICHMENT_MODEL,
440
- };
615
+ return fallbackSuggestion;
441
616
  };
442
617
 
443
618
  export const getCommandConfigEnrichmentServiceConfig = () => ({
619
+ provider: COMMAND_CONFIG_ENRICHMENT_PROVIDER,
444
620
  model: COMMAND_CONFIG_ENRICHMENT_MODEL,
621
+ geminiModel: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
622
+ openaiModel: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
445
623
  timeoutMs: COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS,
446
624
  contextMaxChars: COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS,
447
625
  agentMaxChars: COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS,
448
626
  sourceFileMaxChars: COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS,
449
627
  maxSourceFiles: COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES,
450
628
  baseConfidence: COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE,
629
+ geminiAuthMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
630
+ hasGeminiAuth: isGeminiReady(),
631
+ hasOpenAiApiKey: isOpenAiReady(),
451
632
  hasApiKey: isLlmReady(),
633
+ providerFailureCooldownMs: COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS,
452
634
  });
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import logger from '#logger';
2
3
  import { isSameJidUser } from '../../config/index.js';
3
4
  import { responderPerguntaGlobal } from './globalModuleAiHelpService.js';
@@ -119,7 +120,7 @@ const hasBotKeywordTrigger = (text) => {
119
120
  return tokens.length <= 3;
120
121
  };
121
122
 
122
- const pruneGroupCooldown = (nowMs = Date.now()) => {
123
+ const pruneGroupCooldown = (nowMs = __timeNowMs()) => {
123
124
  for (const [key, expiresAt] of groupCooldownCache.entries()) {
124
125
  if (!Number.isFinite(expiresAt) || expiresAt <= nowMs) {
125
126
  groupCooldownCache.delete(key);
@@ -207,7 +208,7 @@ const shouldSkipForGroupCooldown = ({ chatId, senderJid }) => {
207
208
  if (!key) return false;
208
209
 
209
210
  pruneGroupCooldown();
210
- const nowMs = Date.now();
211
+ const nowMs = __timeNowMs();
211
212
  const expiresAt = groupCooldownCache.get(key) || 0;
212
213
  if (expiresAt > nowMs) return true;
213
214
  return false;
@@ -217,7 +218,7 @@ const markGroupCooldown = ({ chatId, senderJid }) => {
217
218
  const key = buildGroupCooldownKey({ chatId, senderJid });
218
219
  if (!key) return;
219
220
  pruneGroupCooldown();
220
- groupCooldownCache.set(key, Date.now() + GROUP_COOLDOWN_MS);
221
+ groupCooldownCache.set(key, __timeNowMs() + GROUP_COOLDOWN_MS);
221
222
  };
222
223
 
223
224
  const buildIntentFromAnswer = (answer) => ({
@@ -1,5 +1,13 @@
1
+ import { execFile, spawnSync } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
1
4
  const DEFAULT_GEMINI_API_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
2
- export const DEFAULT_GEMINI_MODEL = 'gemini-1.5-flash';
5
+ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
6
+ const DEFAULT_GEMINI_AUTH_MODE = 'auto';
7
+ const DEFAULT_GEMINI_CLI_COMMAND = 'gemini';
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const cliAvailabilityCache = new Map();
3
11
 
4
12
  const normalizeModelName = (value, fallback = DEFAULT_GEMINI_MODEL) => {
5
13
  const raw = String(value || '').trim();
@@ -13,6 +21,64 @@ const toPositiveInt = (value, fallback, min = 1) => {
13
21
  return parsed;
14
22
  };
15
23
 
24
+ const normalizeAuthMode = (value, fallback = DEFAULT_GEMINI_AUTH_MODE) => {
25
+ const normalized = String(value || '')
26
+ .trim()
27
+ .toLowerCase();
28
+ if (normalized === 'api_key') return 'api_key';
29
+ if (normalized === 'cli') return 'cli';
30
+ if (normalized === 'auto') return 'auto';
31
+ return fallback;
32
+ };
33
+
34
+ const normalizeCliCommand = (value) => {
35
+ const command = String(value || DEFAULT_GEMINI_CLI_COMMAND).trim();
36
+ return command || DEFAULT_GEMINI_CLI_COMMAND;
37
+ };
38
+
39
+ const parseBooleanEnv = (value, fallback = false) => {
40
+ if (value === undefined || value === null || value === '') return fallback;
41
+ const normalized = String(value).trim().toLowerCase();
42
+ if (['1', 'true', 'yes', 'sim', 'on'].includes(normalized)) return true;
43
+ if (['0', 'false', 'no', 'nao', 'não', 'off'].includes(normalized)) return false;
44
+ return fallback;
45
+ };
46
+
47
+ const isGeminiCliAvailable = (cliCommand = DEFAULT_GEMINI_CLI_COMMAND) => {
48
+ const safeCliCommand = normalizeCliCommand(cliCommand);
49
+ if (cliAvailabilityCache.has(safeCliCommand)) {
50
+ return cliAvailabilityCache.get(safeCliCommand);
51
+ }
52
+
53
+ let available = false;
54
+ try {
55
+ const result = spawnSync(safeCliCommand, ['--version'], {
56
+ stdio: 'ignore',
57
+ shell: false,
58
+ });
59
+ available = result?.status === 0;
60
+ } catch {
61
+ available = false;
62
+ }
63
+
64
+ cliAvailabilityCache.set(safeCliCommand, available);
65
+ return available;
66
+ };
67
+
68
+ const sanitizeCliOutput = (value) =>
69
+ String(value || '')
70
+ .replaceAll('\0', '')
71
+ .replace(/\r\n/g, '\n')
72
+ .trim();
73
+
74
+ const buildCliPrompt = ({ instructions = '', userPrompt = '' } = {}) => {
75
+ const safePrompt = normalizeOutboundText(userPrompt);
76
+ const safeInstructions = normalizeOutboundText(instructions);
77
+ if (!safeInstructions) return safePrompt;
78
+ if (!safePrompt) return safeInstructions;
79
+ return `${safeInstructions}\n\n---\n\n${safePrompt}`;
80
+ };
81
+
16
82
  const parseErrorMessage = (payload, status) => {
17
83
  const explicit = String(payload?.error?.message || '').trim();
18
84
  if (explicit) return explicit;
@@ -35,14 +101,25 @@ const normalizeOutboundText = (value) =>
35
101
  .join('')
36
102
  .trim();
37
103
 
38
- export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, defaultModel = process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL, timeoutMs = 25_000, apiBaseUrl = process.env.GEMINI_API_BASE_URL || DEFAULT_GEMINI_API_BASE_URL } = {}) => {
104
+ export const isGeminiAuthReady = ({ authMode = process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, apiKey = process.env.GEMINI_API_KEY, cliCommand = process.env.GEMINI_CLI_COMMAND || DEFAULT_GEMINI_CLI_COMMAND } = {}) => {
105
+ const mode = normalizeAuthMode(authMode, DEFAULT_GEMINI_AUTH_MODE);
39
106
  const safeApiKey = String(apiKey || '').trim();
40
- if (!safeApiKey) return null;
107
+ const useCliByEnv = parseBooleanEnv(process.env.GEMINI_USE_CLI_AUTH, false);
108
+ const modeWithLegacy = mode === 'auto' && useCliByEnv ? 'cli' : mode;
41
109
 
42
- if (typeof globalThis.fetch !== 'function') {
43
- throw new Error('createGeminiTextService: global fetch indisponivel no runtime atual.');
44
- }
110
+ if (modeWithLegacy === 'api_key') return Boolean(safeApiKey);
111
+ if (modeWithLegacy === 'cli') return isGeminiCliAvailable(cliCommand);
112
+ if (safeApiKey) return true;
113
+ return isGeminiCliAvailable(cliCommand);
114
+ };
45
115
 
116
+ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, defaultModel = process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL, timeoutMs = 25_000, apiBaseUrl = process.env.GEMINI_API_BASE_URL || DEFAULT_GEMINI_API_BASE_URL, authMode = process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, cliCommand = process.env.GEMINI_CLI_COMMAND || DEFAULT_GEMINI_CLI_COMMAND, execFileAsyncImpl = execFileAsync, isCliAvailableImpl = isGeminiCliAvailable } = {}) => {
117
+ const safeApiKey = String(apiKey || '').trim();
118
+ const safeAuthMode = normalizeAuthMode(authMode, DEFAULT_GEMINI_AUTH_MODE);
119
+ const useCliByEnv = parseBooleanEnv(process.env.GEMINI_USE_CLI_AUTH, false);
120
+ const normalizedAuthMode = safeAuthMode === 'auto' && useCliByEnv ? 'cli' : safeAuthMode;
121
+ const selectedTransport = normalizedAuthMode === 'api_key' ? 'api_key' : normalizedAuthMode === 'cli' ? 'cli' : safeApiKey ? 'api_key' : 'cli';
122
+ const safeCliCommand = normalizeCliCommand(cliCommand);
46
123
  const safeBaseUrl =
47
124
  String(apiBaseUrl || DEFAULT_GEMINI_API_BASE_URL)
48
125
  .trim()
@@ -50,7 +127,16 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
50
127
  const safeTimeoutMs = Math.max(1_000, toPositiveInt(timeoutMs, 25_000, 1_000));
51
128
  const resolvedDefaultModel = normalizeModelName(defaultModel, DEFAULT_GEMINI_MODEL);
52
129
 
53
- const generateText = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
130
+ if (selectedTransport === 'api_key') {
131
+ if (!safeApiKey) return null;
132
+ if (typeof globalThis.fetch !== 'function') {
133
+ throw new Error('createGeminiTextService: global fetch indisponivel no runtime atual.');
134
+ }
135
+ } else if (!isCliAvailableImpl(safeCliCommand)) {
136
+ return null;
137
+ }
138
+
139
+ const generateTextViaApiKey = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
54
140
  const safePrompt = normalizeOutboundText(userPrompt);
55
141
  if (!safePrompt) return { text: '', model: normalizeModelName(model, resolvedDefaultModel) };
56
142
 
@@ -82,6 +168,7 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
82
168
  : null;
83
169
 
84
170
  try {
171
+ // lgtm[js/file-access-to-http]
85
172
  const response = await globalThis.fetch(endpoint, {
86
173
  method: 'POST',
87
174
  headers: {
@@ -108,8 +195,46 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
108
195
  }
109
196
  };
110
197
 
198
+ const generateTextViaCli = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
199
+ const prompt = buildCliPrompt({ instructions, userPrompt });
200
+ const modelName = normalizeModelName(model, resolvedDefaultModel);
201
+ if (!prompt) return { text: '', model: modelName };
202
+
203
+ const args = ['-m', modelName, '-p', prompt, '--output-format', 'text'];
204
+ try {
205
+ const result = await execFileAsyncImpl(safeCliCommand, args, {
206
+ timeout: safeTimeoutMs,
207
+ maxBuffer: 2 * 1024 * 1024,
208
+ env: process.env,
209
+ });
210
+ const text = sanitizeCliOutput(result?.stdout);
211
+ if (!text) {
212
+ const stderrText = sanitizeCliOutput(result?.stderr);
213
+ throw new Error(stderrText || 'Gemini CLI retornou resposta vazia.');
214
+ }
215
+ return {
216
+ text,
217
+ model: modelName,
218
+ };
219
+ } catch (error) {
220
+ const stderrText = sanitizeCliOutput(error?.stderr);
221
+ const stdoutText = sanitizeCliOutput(error?.stdout);
222
+ const baseMessage = String(error?.message || '').trim();
223
+ const finalMessage = stderrText || stdoutText || baseMessage || 'Falha ao executar Gemini CLI.';
224
+ throw new Error(finalMessage);
225
+ }
226
+ };
227
+
228
+ const generateText = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
229
+ if (selectedTransport === 'api_key') {
230
+ return generateTextViaApiKey({ instructions, userPrompt, model });
231
+ }
232
+ return generateTextViaCli({ instructions, userPrompt, model });
233
+ };
234
+
111
235
  return {
112
236
  defaultModel: resolvedDefaultModel,
237
+ transport: selectedTransport,
113
238
  generateText,
114
239
  };
115
240
  };
@@ -2,8 +2,8 @@ import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { createGeminiTextService } from './geminiService.js';
4
4
 
5
- test('createGeminiTextService retorna null quando GEMINI_API_KEY nao existe', () => {
6
- const service = createGeminiTextService({ apiKey: '' });
5
+ test('createGeminiTextService retorna null no modo api_key quando GEMINI_API_KEY nao existe', () => {
6
+ const service = createGeminiTextService({ authMode: 'api_key', apiKey: '' });
7
7
  assert.equal(service, null);
8
8
  });
9
9
 
@@ -85,3 +85,60 @@ test('createGeminiTextService propaga erro detalhado da API', async (t) => {
85
85
  /Modelo invalido/,
86
86
  );
87
87
  });
88
+
89
+ test('createGeminiTextService usa Gemini CLI quando authMode=cli', async () => {
90
+ const calls = [];
91
+ const service = createGeminiTextService({
92
+ authMode: 'cli',
93
+ cliCommand: 'gemini',
94
+ defaultModel: 'gemini-2.5-flash',
95
+ isCliAvailableImpl: () => true,
96
+ execFileAsyncImpl: async (file, args, options) => {
97
+ calls.push({ file, args, options });
98
+ return {
99
+ stdout: 'OK_GEMINI_CLI\n',
100
+ stderr: 'Loaded cached credentials.',
101
+ };
102
+ },
103
+ });
104
+
105
+ assert.ok(service);
106
+ assert.equal(service.transport, 'cli');
107
+
108
+ const response = await service.generateText({
109
+ instructions: 'Responda curto.',
110
+ userPrompt: 'Diga OK.',
111
+ model: 'gemini-2.5-flash',
112
+ });
113
+
114
+ assert.equal(response.model, 'gemini-2.5-flash');
115
+ assert.equal(response.text, 'OK_GEMINI_CLI');
116
+ assert.equal(calls.length, 1);
117
+ assert.equal(calls[0].file, 'gemini');
118
+ assert.ok(calls[0].args.includes('-m'));
119
+ assert.ok(calls[0].args.includes('gemini-2.5-flash'));
120
+ assert.ok(calls[0].args.includes('-p'));
121
+ assert.ok(calls[0].args.includes('--output-format'));
122
+ });
123
+
124
+ test('createGeminiTextService propaga erro do Gemini CLI', async () => {
125
+ const service = createGeminiTextService({
126
+ authMode: 'cli',
127
+ cliCommand: 'gemini',
128
+ defaultModel: 'gemini-2.5-flash',
129
+ isCliAvailableImpl: () => true,
130
+ execFileAsyncImpl: async () => {
131
+ const error = new Error('Command failed');
132
+ error.stderr = 'ModelNotFoundError: Requested entity was not found.';
133
+ throw error;
134
+ },
135
+ });
136
+
137
+ await assert.rejects(
138
+ () =>
139
+ service.generateText({
140
+ userPrompt: 'teste',
141
+ }),
142
+ /ModelNotFoundError/,
143
+ );
144
+ });
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
 
@@ -283,7 +284,7 @@ const ensureFeedbackStoreLoaded = async () => {
283
284
 
284
285
  const persistFeedbackStore = async () => {
285
286
  const store = await ensureFeedbackStoreLoaded();
286
- store.updatedAt = new Date().toISOString();
287
+ store.updatedAt = __timeNowIso();
287
288
  await withFeedbackWrite(async () => {
288
289
  await fs.writeFile(GLOBAL_HELP_FEEDBACK_FILE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
289
290
  });
@@ -1340,7 +1341,7 @@ export const registerGlobalHelpCommandExecution = async ({ chatId, userId, isGro
1340
1341
  } else {
1341
1342
  entry.miss_count = Number(entry.miss_count || 0) + 1;
1342
1343
  }
1343
- entry.last_updated_at = new Date().toISOString();
1344
+ entry.last_updated_at = __timeNowIso();
1344
1345
  await persistFeedbackStore();
1345
1346
 
1346
1347
  setConversationSessionIntent({
@@ -329,6 +329,7 @@ export const executeMessageCommandRoute = async ({ command, args = [], text = ''
329
329
  messageInfo,
330
330
  expirationMessage,
331
331
  senderJid,
332
+ senderIdentity,
332
333
  args: safeArgs,
333
334
  isGroupMessage,
334
335
  commandPrefix,
@@ -377,7 +378,7 @@ export const executeMessageCommandRoute = async ({ command, args = [], text = ''
377
378
  });
378
379
  commandResult = await runCommand('unknown', () =>
379
380
  sendReply(sock, remoteJid, messageInfo, expirationMessage, {
380
- text: globalSuggestion ? `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\n${globalSuggestion}\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista geral de comandos.\n\n🚧 *Fase Beta* \nO omnizap-system ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.` : `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista de comandos disponíveis.\n\n🚧 *Fase Beta* \nO omnizap-system ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.`,
381
+ text: globalSuggestion ? `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\n${globalSuggestion}\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista geral de comandos.\n\n🚧 *Fase Beta* \nO omnizap ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.` : `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista de comandos disponíveis.\n\n🚧 *Fase Beta* \nO omnizap ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.`,
381
382
  }),
382
383
  );
383
384
  break;