@omnizap-system/omnizap 2.6.1 → 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 (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -32,6 +32,8 @@ const METRICS_SERVICE = process.env.METRICS_SERVICE_NAME || process.env.ECOSYSTE
32
32
  const HTTP_SLO_TARGET_MS = Math.max(50, parseEnvNumber(process.env.HTTP_SLO_TARGET_MS, 750));
33
33
 
34
34
  const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLDS, [500, 1000]);
35
+ const ADMIN_ALERT_SEVERITIES = Object.freeze(['critical', 'high', 'medium', 'low', 'unknown']);
36
+ const ADMIN_FEATURE_FLAG_STATES = Object.freeze(['enabled', 'disabled', 'total']);
35
37
 
36
38
  const registry = new client.Registry();
37
39
  let metrics = null;
@@ -375,6 +377,53 @@ const ensureMetrics = () => {
375
377
  labelNames: ['outcome'],
376
378
  registers: [registry],
377
379
  }),
380
+ adminOverviewUpdatedAtSeconds: new client.Gauge({
381
+ name: 'omnizap_admin_overview_updated_at_seconds',
382
+ help: 'Timestamp Unix (s) da ultima atualizacao de snapshot do painel admin',
383
+ registers: [registry],
384
+ }),
385
+ adminOverviewRequestsTotal: new client.Counter({
386
+ name: 'omnizap_admin_overview_requests_total',
387
+ help: 'Total de snapshots do painel admin publicados em metricas',
388
+ labelNames: ['source'],
389
+ registers: [registry],
390
+ }),
391
+ adminCounters: new client.Gauge({
392
+ name: 'omnizap_admin_counters',
393
+ help: 'Contadores agregados do painel admin',
394
+ labelNames: ['counter'],
395
+ registers: [registry],
396
+ }),
397
+ adminDashboardQuick: new client.Gauge({
398
+ name: 'omnizap_admin_dashboard_quick',
399
+ help: 'Metricas rapidas do dashboard admin',
400
+ labelNames: ['metric'],
401
+ registers: [registry],
402
+ }),
403
+ adminSystemHealth: new client.Gauge({
404
+ name: 'omnizap_admin_system_health',
405
+ help: 'Indicadores de saude expostos no painel admin',
406
+ labelNames: ['metric'],
407
+ registers: [registry],
408
+ }),
409
+ adminAlertsTotal: new client.Gauge({
410
+ name: 'omnizap_admin_alerts_total',
411
+ help: 'Total de alertas ativos por severidade no painel admin',
412
+ labelNames: ['severity'],
413
+ registers: [registry],
414
+ }),
415
+ adminFeatureFlagsTotal: new client.Gauge({
416
+ name: 'omnizap_admin_feature_flags_total',
417
+ help: 'Distribuicao de feature flags no painel admin',
418
+ labelNames: ['state'],
419
+ registers: [registry],
420
+ }),
421
+ adminSnapshotItemsTotal: new client.Gauge({
422
+ name: 'omnizap_admin_snapshot_items_total',
423
+ help: 'Total de itens por secao no snapshot do painel admin',
424
+ labelNames: ['section'],
425
+ registers: [registry],
426
+ }),
378
427
  };
379
428
 
380
429
  return metrics;
@@ -502,6 +551,93 @@ export const recordMessagesUpsert = ({ durationMs, type, messagesCount, ok }) =>
502
551
  }
503
552
  };
504
553
 
554
+ export const setAdminOverviewSnapshot = ({ overview = null, source = 'admin_overview' } = {}) => {
555
+ const m = ensureMetrics();
556
+ if (!m) return;
557
+ if (!overview || typeof overview !== 'object') return;
558
+
559
+ const setGaugeIfFinite = (gauge, labels, value, { clampToZero = false } = {}) => {
560
+ const numeric = Number(value);
561
+ if (!Number.isFinite(numeric)) return;
562
+ gauge.set(labels, clampToZero ? Math.max(0, numeric) : numeric);
563
+ };
564
+
565
+ const sourceLabel = normalizeLabel(source, 'admin_overview').slice(0, 32);
566
+ m.adminOverviewRequestsTotal.inc({ source: sourceLabel });
567
+
568
+ const updatedAtMs = Date.parse(String(overview?.updated_at || ''));
569
+ const updatedAtSeconds = Number.isFinite(updatedAtMs) ? updatedAtMs / 1000 : Date.now() / 1000;
570
+ m.adminOverviewUpdatedAtSeconds.set(updatedAtSeconds);
571
+
572
+ const counters = overview?.counters && typeof overview.counters === 'object' ? overview.counters : {};
573
+ const counterKeys = ['total_packs_any_status', 'total_stickers_any_status', 'active_google_sessions', 'known_google_users', 'active_bans', 'visit_events_24h', 'visit_events_7d', 'unique_visitors_7d'];
574
+ for (const key of counterKeys) {
575
+ setGaugeIfFinite(m.adminCounters, { counter: key }, counters?.[key], { clampToZero: true });
576
+ }
577
+
578
+ const dashboardQuick = overview?.dashboard_quick && typeof overview.dashboard_quick === 'object' ? overview.dashboard_quick : {};
579
+ const quickKeys = ['bots_online', 'messages_today', 'spam_blocked_today', 'errors_5xx'];
580
+ for (const key of quickKeys) {
581
+ setGaugeIfFinite(m.adminDashboardQuick, { metric: key }, dashboardQuick?.[key], { clampToZero: true });
582
+ }
583
+
584
+ const systemHealth = overview?.system_health && typeof overview.system_health === 'object' ? overview.system_health : {};
585
+ const healthKeys = ['cpu_percent', 'ram_percent', 'http_latency_p95_ms', 'queue_pending', 'db_total_queries', 'db_slow_queries'];
586
+ for (const key of healthKeys) {
587
+ setGaugeIfFinite(m.adminSystemHealth, { metric: key }, systemHealth?.[key], { clampToZero: true });
588
+ }
589
+
590
+ const moderationQueue = Array.isArray(overview?.moderation_queue) ? overview.moderation_queue : [];
591
+ const auditLog = Array.isArray(overview?.audit_log) ? overview.audit_log : [];
592
+ const users = Array.isArray(overview?.users_sessions?.users) ? overview.users_sessions.users : [];
593
+ const activeSessions = Array.isArray(overview?.users_sessions?.active_sessions) ? overview.users_sessions.active_sessions : [];
594
+ const blockedAccounts = Array.isArray(overview?.users_sessions?.blocked_accounts) ? overview.users_sessions.blocked_accounts : [];
595
+ const alerts = Array.isArray(overview?.alerts) ? overview.alerts : [];
596
+ const featureFlags = Array.isArray(overview?.feature_flags) ? overview.feature_flags : [];
597
+
598
+ m.adminSnapshotItemsTotal.set({ section: 'moderation_queue' }, moderationQueue.length);
599
+ m.adminSnapshotItemsTotal.set({ section: 'audit_log' }, auditLog.length);
600
+ m.adminSnapshotItemsTotal.set({ section: 'users' }, users.length);
601
+ m.adminSnapshotItemsTotal.set({ section: 'active_sessions' }, activeSessions.length);
602
+ m.adminSnapshotItemsTotal.set({ section: 'blocked_accounts' }, blockedAccounts.length);
603
+ m.adminSnapshotItemsTotal.set({ section: 'alerts' }, alerts.length);
604
+ m.adminSnapshotItemsTotal.set({ section: 'feature_flags' }, featureFlags.length);
605
+
606
+ const severityCounts = Object.fromEntries(ADMIN_ALERT_SEVERITIES.map((severity) => [severity, 0]));
607
+ for (const alert of alerts) {
608
+ const severityRaw = String(alert?.severity || '')
609
+ .trim()
610
+ .toLowerCase();
611
+ const severity = ADMIN_ALERT_SEVERITIES.includes(severityRaw) ? severityRaw : 'unknown';
612
+ severityCounts[severity] = Number(severityCounts[severity] || 0) + 1;
613
+ }
614
+ for (const severity of ADMIN_ALERT_SEVERITIES) {
615
+ m.adminAlertsTotal.set({ severity }, Number(severityCounts[severity] || 0));
616
+ }
617
+
618
+ let enabledFlags = 0;
619
+ let disabledFlags = 0;
620
+ for (const flag of featureFlags) {
621
+ if (flag?.is_enabled) {
622
+ enabledFlags += 1;
623
+ } else {
624
+ disabledFlags += 1;
625
+ }
626
+ }
627
+ const totalFlags = enabledFlags + disabledFlags;
628
+ for (const state of ADMIN_FEATURE_FLAG_STATES) {
629
+ if (state === 'enabled') {
630
+ m.adminFeatureFlagsTotal.set({ state }, enabledFlags);
631
+ continue;
632
+ }
633
+ if (state === 'disabled') {
634
+ m.adminFeatureFlagsTotal.set({ state }, disabledFlags);
635
+ continue;
636
+ }
637
+ m.adminFeatureFlagsTotal.set({ state }, totalFlags);
638
+ }
639
+ };
640
+
505
641
  export const recordRpgPlayerCreated = (value = 1) => {
506
642
  const m = ensureMetrics();
507
643
  if (!m) return;
@@ -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,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
 
@@ -109,8 +195,46 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
109
195
  }
110
196
  };
111
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
+
112
235
  return {
113
236
  defaultModel: resolvedDefaultModel,
237
+ transport: selectedTransport,
114
238
  generateText,
115
239
  };
116
240
  };