@ssolis-ti/mcp-compra-agil 1.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +113 -25
  2. package/dist/api/compra-agil-client.d.ts +2 -1
  3. package/dist/api/compra-agil-client.d.ts.map +1 -1
  4. package/dist/api/compra-agil-client.js +12 -2
  5. package/dist/api/compra-agil-client.js.map +1 -1
  6. package/dist/index.js +31 -7
  7. package/dist/index.js.map +1 -1
  8. package/dist/reports/components.d.ts +59 -0
  9. package/dist/reports/components.d.ts.map +1 -0
  10. package/dist/reports/components.js +125 -0
  11. package/dist/reports/components.js.map +1 -0
  12. package/dist/reports/export.d.ts +27 -0
  13. package/dist/reports/export.d.ts.map +1 -0
  14. package/dist/reports/export.js +47 -0
  15. package/dist/reports/export.js.map +1 -0
  16. package/dist/reports/format.d.ts +24 -0
  17. package/dist/reports/format.d.ts.map +1 -0
  18. package/dist/reports/format.js +74 -0
  19. package/dist/reports/format.js.map +1 -0
  20. package/dist/reports/render.d.ts +18 -0
  21. package/dist/reports/render.d.ts.map +1 -0
  22. package/dist/reports/render.js +30 -0
  23. package/dist/reports/render.js.map +1 -0
  24. package/dist/reports/templates/radar-oportunidades.d.ts +41 -0
  25. package/dist/reports/templates/radar-oportunidades.d.ts.map +1 -0
  26. package/dist/reports/templates/radar-oportunidades.js +170 -0
  27. package/dist/reports/templates/radar-oportunidades.js.map +1 -0
  28. package/dist/reports/theme.d.ts +83 -0
  29. package/dist/reports/theme.d.ts.map +1 -0
  30. package/dist/reports/theme.js +284 -0
  31. package/dist/reports/theme.js.map +1 -0
  32. package/dist/resources/compras-template.d.ts.map +1 -1
  33. package/dist/resources/compras-template.js +2 -1
  34. package/dist/resources/compras-template.js.map +1 -1
  35. package/dist/resources/documentacion.d.ts.map +1 -1
  36. package/dist/resources/documentacion.js +5 -36
  37. package/dist/resources/documentacion.js.map +1 -1
  38. package/dist/resources/estados.d.ts.map +1 -1
  39. package/dist/resources/estados.js +24 -10
  40. package/dist/resources/estados.js.map +1 -1
  41. package/dist/services/monitor.js +37 -2
  42. package/dist/services/monitor.js.map +1 -1
  43. package/dist/tools/analizar-precios-mercado.d.ts +21 -0
  44. package/dist/tools/analizar-precios-mercado.d.ts.map +1 -0
  45. package/dist/tools/analizar-precios-mercado.js +217 -0
  46. package/dist/tools/analizar-precios-mercado.js.map +1 -0
  47. package/dist/tools/auditar-desiertas.d.ts.map +1 -1
  48. package/dist/tools/auditar-desiertas.js +64 -46
  49. package/dist/tools/auditar-desiertas.js.map +1 -1
  50. package/dist/tools/buscar-compras.d.ts.map +1 -1
  51. package/dist/tools/buscar-compras.js +2 -1
  52. package/dist/tools/buscar-compras.js.map +1 -1
  53. package/dist/tools/detalle-compra.d.ts.map +1 -1
  54. package/dist/tools/detalle-compra.js +6 -5
  55. package/dist/tools/detalle-compra.js.map +1 -1
  56. package/dist/tools/detalle-oc.d.ts.map +1 -1
  57. package/dist/tools/detalle-oc.js +6 -5
  58. package/dist/tools/detalle-oc.js.map +1 -1
  59. package/dist/tools/documentos.d.ts.map +1 -1
  60. package/dist/tools/documentos.js +51 -50
  61. package/dist/tools/documentos.js.map +1 -1
  62. package/dist/tools/generar-borrador.d.ts.map +1 -1
  63. package/dist/tools/generar-borrador.js +37 -26
  64. package/dist/tools/generar-borrador.js.map +1 -1
  65. package/dist/tools/generar-informe.d.ts +13 -0
  66. package/dist/tools/generar-informe.d.ts.map +1 -0
  67. package/dist/tools/generar-informe.js +111 -0
  68. package/dist/tools/generar-informe.js.map +1 -0
  69. package/dist/tools/monitorear-cambios.d.ts +32 -2
  70. package/dist/tools/monitorear-cambios.d.ts.map +1 -1
  71. package/dist/tools/monitorear-cambios.js +79 -10
  72. package/dist/tools/monitorear-cambios.js.map +1 -1
  73. package/dist/tools/radar-oportunidades.d.ts +42 -1
  74. package/dist/tools/radar-oportunidades.d.ts.map +1 -1
  75. package/dist/tools/radar-oportunidades.js +123 -103
  76. package/dist/tools/radar-oportunidades.js.map +1 -1
  77. package/dist/tools/recomendar-precio.d.ts +13 -0
  78. package/dist/tools/recomendar-precio.d.ts.map +1 -1
  79. package/dist/tools/recomendar-precio.js +54 -58
  80. package/dist/tools/recomendar-precio.js.map +1 -1
  81. package/dist/tools/verificar-oc.d.ts.map +1 -1
  82. package/dist/tools/verificar-oc.js +9 -7
  83. package/dist/tools/verificar-oc.js.map +1 -1
  84. package/dist/tools/verificar-ticket.d.ts +13 -0
  85. package/dist/tools/verificar-ticket.d.ts.map +1 -0
  86. package/dist/tools/verificar-ticket.js +79 -0
  87. package/dist/tools/verificar-ticket.js.map +1 -0
  88. package/dist/utils/docs-locator.d.ts +41 -0
  89. package/dist/utils/docs-locator.d.ts.map +1 -0
  90. package/dist/utils/docs-locator.js +82 -0
  91. package/dist/utils/docs-locator.js.map +1 -0
  92. package/dist/utils/error-handler.d.ts.map +1 -1
  93. package/dist/utils/error-handler.js +7 -4
  94. package/dist/utils/error-handler.js.map +1 -1
  95. package/dist/utils/logger.d.ts.map +1 -1
  96. package/dist/utils/logger.js +9 -6
  97. package/dist/utils/logger.js.map +1 -1
  98. package/dist/utils/quotation.d.ts +71 -0
  99. package/dist/utils/quotation.d.ts.map +1 -0
  100. package/dist/utils/quotation.js +140 -0
  101. package/dist/utils/quotation.js.map +1 -0
  102. package/dist/utils/rate-limiter.d.ts +8 -1
  103. package/dist/utils/rate-limiter.d.ts.map +1 -1
  104. package/dist/utils/rate-limiter.js +25 -1
  105. package/dist/utils/rate-limiter.js.map +1 -1
  106. package/dist/utils/redact.d.ts +48 -0
  107. package/dist/utils/redact.d.ts.map +1 -0
  108. package/dist/utils/redact.js +100 -0
  109. package/dist/utils/redact.js.map +1 -0
  110. package/package.json +55 -53
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Tool: generar_informe
3
+ *
4
+ * Genera informes profesionales (HTML imprimible) a partir de los datos del MCP.
5
+ *
6
+ * DISEÑO: el HTML nunca se devuelve al LLM — se escribe a disco y la tool
7
+ * retorna solo la ruta y un resumen breve. Un informe pesa decenas de KB y
8
+ * retornarlo consumiría miles de tokens de contexto por llamada.
9
+ */
10
+ import { z } from 'zod';
11
+ import { CompraAgilApiError } from '../utils/error-handler.js';
12
+ import { logger } from '../utils/logger.js';
13
+ import { recolectarDatosRadar } from './radar-oportunidades.js';
14
+ import { renderRadarInforme } from '../reports/templates/radar-oportunidades.js';
15
+ import { escribirInforme, slug, stamp, defaultOutputDir } from '../reports/export.js';
16
+ import { clp } from '../reports/format.js';
17
+ import { PAPEL, FORMATO_POR_DEFECTO } from '../reports/theme.js';
18
+ import { safeError } from '../utils/redact.js';
19
+ const TOOL_NAME = 'generar_informe';
20
+ const TOOL_DESCRIPTION = `Genera un informe profesional imprimible (HTML con diseño de impresión) a partir de los datos de Compra Ágil.
21
+ El archivo se guarda en disco y la herramienta devuelve la RUTA del archivo (no su contenido, para no saturar el contexto).
22
+ El usuario puede abrirlo en su navegador y exportarlo a PDF con Ctrl+P (el diseño está optimizado para impresión: saltos de página controlados y cabeceras de tabla repetidas).
23
+ Formatos de papel: "carta" (216×279mm, estándar de oficina en Chile, por defecto), "oficio" (216×330mm, folio chileno para documentos oficiales) y "a4" (210×297mm, estándar ISO).
24
+ Tipos disponibles:
25
+ - "radar": Radar de oportunidades activas priorizadas por facilidad de adjudicación (KPIs, gráfico de puntuación, fichas destacadas y listado completo).`;
26
+ const inputSchema = {
27
+ tipo: z.enum(['radar']).describe('Tipo de informe a generar. Actualmente disponible: "radar".'),
28
+ formato_papel: z.enum(['carta', 'oficio', 'a4']).default('carta').optional().describe('Tamaño de papel: "carta" (216×279mm, el más usado en oficinas chilenas, por defecto), "oficio" (216×330mm, folio chileno para documentos oficiales/legales) o "a4" (210×297mm, estándar ISO).'),
29
+ region: z.string().optional().describe('Código de región para acotar (1-16). Ej: "13" para Metropolitana.'),
30
+ q: z.string().optional().describe('Término de búsqueda para acotar a un rubro o producto (ej: "licencias").'),
31
+ presupuesto_minimo: z.number().optional().describe('Filtrar procesos con presupuesto disponible mayor o igual a este monto en CLP.'),
32
+ limite_resultados: z.number().min(1).max(50).default(20).optional().describe('Cantidad máxima de oportunidades a incluir en el informe (1-50, default 20).'),
33
+ max_paginas: z.number().min(1).max(10).default(3).optional().describe('Páginas de 50 resultados a escanear (1-10, default 3).'),
34
+ ruta_salida: z.string().optional().describe('Directorio donde guardar el informe. Si se omite, se usa la carpeta "informes/" del directorio de trabajo.'),
35
+ };
36
+ export function registerGenerarInforme(server, client) {
37
+ server.registerTool(TOOL_NAME, {
38
+ description: TOOL_DESCRIPTION,
39
+ inputSchema: inputSchema,
40
+ }, async (args) => {
41
+ try {
42
+ if (args.tipo !== 'radar') {
43
+ return {
44
+ content: [{ type: 'text', text: `Tipo de informe no soportado: "${args.tipo}".` }],
45
+ isError: true,
46
+ };
47
+ }
48
+ logger.info(`generar_informe: construyendo informe "${args.tipo}"`);
49
+ const generadoEn = new Date();
50
+ // 1. Recolectar datos con la MISMA función que usa la tool JSON del radar
51
+ const datos = await recolectarDatosRadar(client, {
52
+ region: args.region,
53
+ q: args.q,
54
+ presupuesto_minimo: args.presupuesto_minimo,
55
+ limite_resultados: args.limite_resultados ?? 20,
56
+ max_paginas: args.max_paginas,
57
+ }, generadoEn.getTime());
58
+ // 2. Renderizar el HTML en el formato de papel solicitado
59
+ const formato = args.formato_papel ?? FORMATO_POR_DEFECTO;
60
+ const html = renderRadarInforme({
61
+ oportunidades: datos.oportunidades,
62
+ totalAnalizadas: datos.totalAnalizadas,
63
+ filtros: {
64
+ region: args.region,
65
+ q: args.q,
66
+ presupuestoMinimo: args.presupuesto_minimo,
67
+ paginasEscaneadas: datos.paginasEscaneadas,
68
+ },
69
+ generadoEn,
70
+ formato,
71
+ });
72
+ // 3. Escribir a disco y devolver SOLO la ruta + resumen
73
+ const ambito = args.region ? `region-${args.region}` : 'nacional';
74
+ const rubro = args.q ? `-${slug(args.q)}` : '';
75
+ const nombre = `radar-${ambito}${rubro}-${formato}-${stamp(generadoEn)}.html`;
76
+ const { ruta, bytes } = escribirInforme(html, nombre, args.ruta_salida);
77
+ const ops = datos.oportunidades;
78
+ const sinOferentes = ops.filter((o) => o.ofertas_recibidas === 0).length;
79
+ const montoTotal = ops.reduce((acc, o) => acc + (o.presupuesto_disponible || 0), 0);
80
+ const mejor = ops[0];
81
+ const resumen = [
82
+ `✅ Informe generado: ${ruta}`,
83
+ ` (${(bytes / 1024).toFixed(1)} KB · formato ${PAPEL[formato].glosa})`,
84
+ ` Ábrelo en el navegador y usa Ctrl+P para exportar a PDF.`,
85
+ ` ⚠ En el diálogo de impresión selecciona el papel "${formato === 'carta' ? 'Carta / Letter' : formato === 'oficio' ? 'Oficio / Folio (216×330mm)' : 'A4'}" para que calce exactamente.`,
86
+ ``,
87
+ `Resumen del contenido:`,
88
+ `• ${ops.length} oportunidades incluidas (de ${datos.totalAnalizadas} vigentes analizadas)`,
89
+ `• ${sinOferentes} sin oferentes (competencia cero)`,
90
+ `• ${ops.filter((o) => o.horas_restantes <= 24).length} cierran en menos de 24 horas`,
91
+ `• Monto total en juego: ${clp(montoTotal)}`,
92
+ mejor ? `• Mejor oportunidad: ${mejor.nombre} (${mejor.codigo}) — ${mejor.puntuacion_caliente} pts` : '',
93
+ ``,
94
+ `Carpeta de informes: ${args.ruta_salida ? args.ruta_salida : defaultOutputDir()}`,
95
+ ].filter(Boolean).join('\n');
96
+ return {
97
+ content: [{ type: 'text', text: resumen }],
98
+ };
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof CompraAgilApiError
102
+ ? error.actionableMessage
103
+ : `Error inesperado al generar el informe: ${safeError(error)}`;
104
+ return {
105
+ content: [{ type: 'text', text: message }],
106
+ isError: true,
107
+ };
108
+ }
109
+ });
110
+ }
111
+ //# sourceMappingURL=generar-informe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generar-informe.js","sourceRoot":"","sources":["../../src/tools/generar-informe.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,SAAS,GAAG,iBAAiB,CAAC;AAEpC,MAAM,gBAAgB,GAAG;;;;;yJAKgI,CAAC;AAE1J,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,6DAA6D,CAAC;IAC/F,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CACnF,+LAA+L,CAChM;IACD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mEAAmE,CAAC;IAC3G,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;IAC7G,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gFAAgF,CAAC;IACpI,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8EAA8E,CAAC;IAC5J,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;IAC/H,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4GAA4G,CAAC;CAC1J,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,MAAiB,EAAE,MAAwB;IAChF,MAAM,CAAC,YAAY,CACjB,SAAS,EACT;QACE,WAAW,EAAE,gBAAgB;QAC7B,WAAW,EAAE,WAAW;KACzB,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;QACb,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC1B,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,kCAAkC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;oBAC3F,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,0CAA0C,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YACpE,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC;YAE9B,0EAA0E;YAC1E,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE;gBAC/C,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,CAAC,EAAE,IAAI,CAAC,CAAC;gBACT,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;gBAC3C,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,IAAI,EAAE;gBAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;aAC9B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC;YAEzB,0DAA0D;YAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,IAAI,mBAAmB,CAAC;YAC1D,MAAM,IAAI,GAAG,kBAAkB,CAAC;gBAC9B,aAAa,EAAE,KAAK,CAAC,aAAa;gBAClC,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,OAAO,EAAE;oBACP,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,CAAC,EAAE,IAAI,CAAC,CAAC;oBACT,iBAAiB,EAAE,IAAI,CAAC,kBAAkB;oBAC1C,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;iBAC3C;gBACD,UAAU;gBACV,OAAO;aACR,CAAC,CAAC;YAEH,wDAAwD;YACxD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;YAClE,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,MAAM,MAAM,GAAG,SAAS,MAAM,GAAG,KAAK,IAAI,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC;YAC9E,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YAExE,MAAM,GAAG,GAAG,KAAK,CAAC,aAAa,CAAC;YAChC,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;YACzE,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,sBAAsB,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACpF,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAErB,MAAM,OAAO,GAAG;gBACd,uBAAuB,IAAI,EAAE;gBAC7B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,GAAG;gBACxE,6DAA6D;gBAC7D,wDAAwD,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,IAAI,+BAA+B;gBAC1L,EAAE;gBACF,wBAAwB;gBACxB,KAAK,GAAG,CAAC,MAAM,gCAAgC,KAAK,CAAC,eAAe,uBAAuB;gBAC3F,KAAK,YAAY,mCAAmC;gBACpD,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,MAAM,+BAA+B;gBACrF,2BAA2B,GAAG,CAAC,UAAU,CAAC,EAAE;gBAC5C,KAAK,CAAC,CAAC,CAAC,wBAAwB,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,mBAAmB,MAAM,CAAC,CAAC,CAAC,EAAE;gBACxG,EAAE;gBACF,wBAAwB,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,gBAAgB,EAAE,EAAE;aACnF,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAE7B,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;aACpD,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,kBAAkB;gBACjD,CAAC,CAAC,KAAK,CAAC,iBAAiB;gBACzB,CAAC,CAAC,2CAA2C,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAClE,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;gBACnD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -1,10 +1,40 @@
1
1
  /**
2
2
  * Tool: monitorear_cambios_recientes
3
3
  *
4
- * Detecta procesos de Compra Ágil que hayan sido creados o modificados
5
- * recientemente. Ideal para alertas y monitoreo de oportunidades.
4
+ * Detecta procesos de Compra Ágil creados o modificados dentro de una ventana
5
+ * de cambios: relativa (últimos N minutos) o absoluta (rango de fechas
6
+ * arbitrario). Ideal para alertas, monitoreo de oportunidades y sincronización
7
+ * incremental de un período pasado.
6
8
  */
7
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
10
  import { CompraAgilClient } from '../api/compra-agil-client.js';
11
+ /** Ventana de cambios ya resuelta y lista para enviar a la API. */
12
+ export interface VentanaCambios {
13
+ /** Parámetros de la API que definen la ventana (uno u otro grupo, nunca ambos). */
14
+ params: {
15
+ ttl_cambio_ms?: number;
16
+ cambio_desde?: string;
17
+ cambio_hasta?: string;
18
+ };
19
+ /** Glosa legible para el reporte de salida. */
20
+ descripcion: string;
21
+ }
22
+ /**
23
+ * Resuelve la ventana de cambios a partir de los argumentos de la herramienta.
24
+ *
25
+ * La API expone dos formas mutuamente excluyentes de acotar los cambios
26
+ * (Guía API Compra Ágil v2 §5.1, Grupo 1): `ttl_cambio_ms` (opción A) o el par
27
+ * `cambio_desde`/`cambio_hasta` (opción B). Enviar ambas a la vez es un 400,
28
+ * así que se decide aquí y se falla localmente para no gastar cuota.
29
+ *
30
+ * Retorna un string con el error de validación, o la ventana resuelta.
31
+ */
32
+ export declare function resolverVentanaCambios(args: {
33
+ minutos?: number;
34
+ cambio_desde?: string;
35
+ cambio_hasta?: string;
36
+ }): VentanaCambios | {
37
+ error: string;
38
+ };
9
39
  export declare function registerMonitorearCambios(server: McpServer, client: CompraAgilClient): void;
10
40
  //# sourceMappingURL=monitorear-cambios.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"monitorear-cambios.d.ts","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AA4BhE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAqD3F"}
1
+ {"version":3,"file":"monitorear-cambios.d.ts","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAyChE,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,MAAM,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CA+CrC;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA4D3F"}
@@ -1,31 +1,100 @@
1
1
  /**
2
2
  * Tool: monitorear_cambios_recientes
3
3
  *
4
- * Detecta procesos de Compra Ágil que hayan sido creados o modificados
5
- * recientemente. Ideal para alertas y monitoreo de oportunidades.
4
+ * Detecta procesos de Compra Ágil creados o modificados dentro de una ventana
5
+ * de cambios: relativa (últimos N minutos) o absoluta (rango de fechas
6
+ * arbitrario). Ideal para alertas, monitoreo de oportunidades y sincronización
7
+ * incremental de un período pasado.
6
8
  */
7
9
  import { z } from 'zod';
8
10
  import { CompraAgilApiError } from '../utils/error-handler.js';
11
+ import { safeError } from '../utils/redact.js';
9
12
  const TOOL_NAME = 'monitorear_cambios_recientes';
10
- const TOOL_DESCRIPTION = `Busca Compras Ágiles que hayan sido creadas o modificadas en los últimos N minutos.
11
- Ideal para monitoreo de nuevas oportunidades de negocio y alertas en tiempo real.
12
- Internamente convierte los minutos a milisegundos para el parámetro ttl_cambio_ms de la API.
13
+ const TOOL_DESCRIPTION = `Busca Compras Ágiles que hayan sido creadas o modificadas dentro de una ventana de cambios.
14
+ Ideal para monitoreo de nuevas oportunidades de negocio, alertas en tiempo real y sincronización incremental.
15
+ La ventana se define de UNA de dos formas, mutuamente excluyentes:
16
+ • Relativa: 'minutos' (últimos N minutos, máx 1440 = 24h). Es el modo por defecto (60 min).
17
+ • Absoluta: 'cambio_desde' + 'cambio_hasta' (rango ISO-8601 arbitrario), para resincronizar un período pasado
18
+ o retomar desde el último timestamp procesado, sin el techo de 24 horas.
13
19
  Puede combinarse con filtros de estado y región para acotar los resultados.`;
20
+ /** Acepta ISO-8601 con 'Z' o con offset (±HH:MM). Los segundos son opcionales. */
21
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
14
22
  const inputSchema = {
15
- minutos: z.number().min(1).max(1440).default(60).describe('Buscar cambios en los últimos N minutos. Ej: 60 = última hora, 1440 = últimas 24 horas. Máximo 1440 (24h).'),
23
+ minutos: z.number().min(1).max(1440).optional().describe('Ventana RELATIVA: buscar cambios en los últimos N minutos. Ej: 60 = última hora, 1440 = últimas 24 horas. Máximo 1440 (24h). Se usa 60 por defecto si no se indica ni "minutos" ni un rango. No combinar con "cambio_desde"/"cambio_hasta".'),
24
+ cambio_desde: z.string().optional().describe('Ventana ABSOLUTA: fecha/hora ISO-8601 de inicio del rango de cambios. Ej: "2026-09-01T00:00:00Z". Úsala para sincronización incremental de un período arbitrario (sin el límite de 24h de "minutos"). No combinar con "minutos".'),
25
+ cambio_hasta: z.string().optional().describe('Ventana ABSOLUTA: fecha/hora ISO-8601 de fin del rango de cambios. Ej: "2026-09-02T00:00:00Z". Requiere "cambio_desde". No combinar con "minutos".'),
16
26
  estado: z.string().optional().describe('Filtrar por estado(s), separados por coma. Ej: "publicada" para solo oportunidades abiertas.'),
17
27
  region: z.string().optional().describe('Código(s) de región, separados por coma. Ej: "13" para Metropolitana.'),
18
28
  tamano_pagina: z.number().min(10).max(50).default(50).describe('Resultados por página (10-50, default 50).'),
19
29
  numero_pagina: z.number().min(1).optional().describe('Número de página a consultar (comienza en 1).'),
20
30
  };
31
+ /**
32
+ * Resuelve la ventana de cambios a partir de los argumentos de la herramienta.
33
+ *
34
+ * La API expone dos formas mutuamente excluyentes de acotar los cambios
35
+ * (Guía API Compra Ágil v2 §5.1, Grupo 1): `ttl_cambio_ms` (opción A) o el par
36
+ * `cambio_desde`/`cambio_hasta` (opción B). Enviar ambas a la vez es un 400,
37
+ * así que se decide aquí y se falla localmente para no gastar cuota.
38
+ *
39
+ * Retorna un string con el error de validación, o la ventana resuelta.
40
+ */
41
+ export function resolverVentanaCambios(args) {
42
+ const usaRango = Boolean(args.cambio_desde || args.cambio_hasta);
43
+ if (usaRango && args.minutos !== undefined) {
44
+ return {
45
+ error: 'Error de validación: "minutos" (ventana relativa) y "cambio_desde"/"cambio_hasta" (ventana absoluta) son mutuamente excluyentes. Usa solo uno de los dos modos.',
46
+ };
47
+ }
48
+ if (usaRango) {
49
+ if (!args.cambio_desde) {
50
+ return {
51
+ error: 'Error de validación: "cambio_hasta" requiere también "cambio_desde" para delimitar el rango.',
52
+ };
53
+ }
54
+ for (const [nombre, valor] of [
55
+ ['cambio_desde', args.cambio_desde],
56
+ ['cambio_hasta', args.cambio_hasta],
57
+ ]) {
58
+ if (valor && !ISO_8601.test(valor)) {
59
+ return {
60
+ error: `Error de validación: "${nombre}" debe estar en formato ISO-8601 con zona horaria. Ej: "2026-09-01T00:00:00Z". Recibido: "${valor}".`,
61
+ };
62
+ }
63
+ }
64
+ if (args.cambio_hasta && args.cambio_desde > args.cambio_hasta) {
65
+ return {
66
+ error: `Error de validación: "cambio_desde" (${args.cambio_desde}) es posterior a "cambio_hasta" (${args.cambio_hasta}). El rango quedaría vacío.`,
67
+ };
68
+ }
69
+ return {
70
+ params: { cambio_desde: args.cambio_desde, cambio_hasta: args.cambio_hasta },
71
+ descripcion: args.cambio_hasta
72
+ ? `Cambios entre ${args.cambio_desde} y ${args.cambio_hasta}`
73
+ : `Cambios desde ${args.cambio_desde}`,
74
+ };
75
+ }
76
+ // Modo relativo (por defecto): 60 minutos si no se indicó nada.
77
+ const minutos = args.minutos ?? 60;
78
+ return {
79
+ params: { ttl_cambio_ms: minutos * 60 * 1000 },
80
+ descripcion: `Últimos ${minutos} minutos`,
81
+ };
82
+ }
21
83
  export function registerMonitorearCambios(server, client) {
22
84
  server.registerTool(TOOL_NAME, {
23
85
  description: TOOL_DESCRIPTION,
24
86
  inputSchema: inputSchema,
25
87
  }, async (args) => {
26
88
  try {
27
- const ttlMs = args.minutos * 60 * 1000;
28
- const response = await client.cambiosRecientes(ttlMs, {
89
+ const ventana = resolverVentanaCambios(args);
90
+ if ('error' in ventana) {
91
+ return {
92
+ content: [{ type: 'text', text: ventana.error }],
93
+ isError: true,
94
+ };
95
+ }
96
+ const response = await client.buscar({
97
+ ...ventana.params,
29
98
  estado: args.estado,
30
99
  region: args.region,
31
100
  tamano_pagina: args.tamano_pagina,
@@ -42,7 +111,7 @@ export function registerMonitorearCambios(server, client) {
42
111
  ultimo_cambio: item.fechas.fecha_ultimo_cambio,
43
112
  }));
44
113
  const result = {
45
- ventana_temporal: `Últimos ${args.minutos} minutos`,
114
+ ventana_temporal: ventana.descripcion,
46
115
  total_resultados: response.paginacion.total_resultados,
47
116
  pagina: `${response.paginacion.numero_pagina} de ${response.paginacion.total_paginas}`,
48
117
  resultados: summary,
@@ -57,7 +126,7 @@ export function registerMonitorearCambios(server, client) {
57
126
  catch (error) {
58
127
  const message = error instanceof CompraAgilApiError
59
128
  ? error.actionableMessage
60
- : `Error inesperado: ${String(error)}`;
129
+ : `Error inesperado: ${safeError(error)}`;
61
130
  return {
62
131
  content: [{ type: 'text', text: message }],
63
132
  isError: true,
@@ -1 +1 @@
1
- {"version":3,"file":"monitorear-cambios.js","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"monitorear-cambios.js","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,45 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { CompraAgilClient } from '../api/compra-agil-client.js';
2
+ import { CompraAgilClient, CompraAgilItem } from '../api/compra-agil-client.js';
3
+ export interface OportunidadRadar {
4
+ codigo: string;
5
+ nombre: string;
6
+ organismo: string;
7
+ region: string;
8
+ presupuesto_disponible: number;
9
+ ofertas_recibidas: number;
10
+ horas_restantes: number;
11
+ fecha_cierre: string;
12
+ puntuacion_caliente: number;
13
+ factores_calificacion: string[];
14
+ }
15
+ /**
16
+ * Calcula el "Hot Score" de una oportunidad de forma pura (sin efectos secundarios).
17
+ * Retorna null si el proceso debe omitirse (ya cerrado o bajo el presupuesto mínimo).
18
+ * Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20) + facilidad (máx 5).
19
+ */
20
+ export declare function evaluarOportunidad(item: CompraAgilItem, now: number, minBudget?: number): OportunidadRadar | null;
21
+ export interface RadarParams {
22
+ region?: string;
23
+ q?: string;
24
+ presupuesto_minimo?: number;
25
+ limite_resultados?: number;
26
+ max_paginas?: number;
27
+ }
28
+ export interface RadarDatos {
29
+ /** Oportunidades vigentes ya rankeadas y recortadas al límite pedido. */
30
+ oportunidades: OportunidadRadar[];
31
+ /** Total de oportunidades vigentes encontradas antes de aplicar el límite. */
32
+ totalAnalizadas: number;
33
+ limite: number;
34
+ paginasEscaneadas: number;
35
+ }
36
+ /**
37
+ * Recolecta y rankea las oportunidades del radar.
38
+ *
39
+ * Función pura de datos (sin formato de salida): la comparten la tool
40
+ * `radar_oportunidades_calientes` (que emite JSON para el LLM) y el informe
41
+ * HTML/PDF, garantizando que ambos muestren exactamente lo mismo.
42
+ */
43
+ export declare function recolectarDatosRadar(client: CompraAgilClient, params: RadarParams, now?: number): Promise<RadarDatos>;
3
44
  export declare function registerRadarOportunidades(server: McpServer, client: CompraAgilClient): void;
4
45
  //# sourceMappingURL=radar-oportunidades.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"radar-oportunidades.d.ts","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AA6BhE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAoJ5F"}
1
+ {"version":3,"file":"radar-oportunidades.d.ts","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAkBhF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,MAAM,EACX,SAAS,SAAI,GACZ,gBAAgB,GAAG,IAAI,CAyEzB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,8EAA8E;IAC9E,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,UAAU,CAAC,CA6BrB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA4C5F"}
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { CompraAgilApiError } from '../utils/error-handler.js';
3
3
  import { logger } from '../utils/logger.js';
4
+ import { safeError } from '../utils/redact.js';
4
5
  const TOOL_NAME = 'radar_oportunidades_calientes';
5
6
  const TOOL_DESCRIPTION = `Escanea, califica y clasifica de forma priorizada los procesos de Compra Ágil activos (publicados).
6
7
  Utiliza una fórmula ponderada (Hot Score) basada en la falta de oferentes, el presupuesto disponible y las horas restantes de cierre para destacar los llamados más convenientes y fáciles de ganar.`;
@@ -9,23 +10,131 @@ const inputSchema = {
9
10
  q: z.string().optional().describe('Término de búsqueda opcional para acotar a un rubro o producto específico (ej: "licencias").'),
10
11
  presupuesto_minimo: z.number().optional().describe('Filtrar solo procesos con presupuesto disponible mayor o igual a este monto en CLP.'),
11
12
  limite_resultados: z.number().min(1).max(20).default(10).optional().describe('Cantidad máxima de oportunidades destacadas a retornar (1-20, default 10).'),
13
+ max_paginas: z.number().min(1).max(10).default(3).optional().describe('Cuántas páginas de 50 resultados escanear antes de rankear (1-10, default 3 = hasta 150 procesos). Más páginas = más cobertura pero más consumo de cuota.'),
12
14
  };
15
+ /**
16
+ * Calcula el "Hot Score" de una oportunidad de forma pura (sin efectos secundarios).
17
+ * Retorna null si el proceso debe omitirse (ya cerrado o bajo el presupuesto mínimo).
18
+ * Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20) + facilidad (máx 5).
19
+ */
20
+ export function evaluarOportunidad(item, now, minBudget = 0) {
21
+ const budget = item.montos?.monto_disponible_clp || item.montos?.monto_disponible || 0;
22
+ if (budget < minBudget)
23
+ return null;
24
+ let hoursLeft = 0;
25
+ if (item.fechas?.fecha_cierre) {
26
+ const cierreTime = new Date(item.fechas.fecha_cierre).getTime();
27
+ hoursLeft = (cierreTime - now) / (1000 * 60 * 60);
28
+ }
29
+ if (hoursLeft <= 0)
30
+ return null;
31
+ let score = 0;
32
+ const factors = [];
33
+ // Factor 1: Baja Competencia — Máx 50 pts
34
+ const bids = item.resumen?.total_ofertas_recibidas ?? 0;
35
+ if (bids === 0) {
36
+ score += 50;
37
+ factors.push('Sin oferentes activos (+50 pts)');
38
+ }
39
+ else if (bids === 1) {
40
+ score += 30;
41
+ factors.push('Baja competencia: solo 1 oferente (+30 pts)');
42
+ }
43
+ else if (bids === 2) {
44
+ score += 15;
45
+ factors.push('Competencia moderada: 2 oferentes (+15 pts)');
46
+ }
47
+ // Factor 2: Urgencia del Cierre — Máx 30 pts
48
+ if (hoursLeft <= 4) {
49
+ score += 30;
50
+ factors.push('Urgencia crítica: cierra en menos de 4 horas (+30 pts)');
51
+ }
52
+ else if (hoursLeft <= 12) {
53
+ score += 20;
54
+ factors.push('Cierre inminente: cierra en menos de 12 horas (+20 pts)');
55
+ }
56
+ else if (hoursLeft <= 24) {
57
+ score += 10;
58
+ factors.push('Cierre cercano: cierra en menos de 24 horas (+10 pts)');
59
+ }
60
+ // Factor 3: Tamaño de Presupuesto — Máx 20 pts
61
+ if (budget >= 5000000) {
62
+ score += 20;
63
+ factors.push('Presupuesto de alto valor (>= $5.000.000 CLP) (+20 pts)');
64
+ }
65
+ else if (budget >= 2000000) {
66
+ score += 15;
67
+ factors.push('Presupuesto medio-alto (>= $2.000.000 CLP) (+15 pts)');
68
+ }
69
+ else if (budget >= 500000) {
70
+ score += 10;
71
+ factors.push('Presupuesto medio (>= $500.000 CLP) (+10 pts)');
72
+ }
73
+ else {
74
+ score += 5;
75
+ factors.push('Presupuesto bajo (< $500.000 CLP) (+5 pts)');
76
+ }
77
+ // Factor 4: Facilidad de Postulación — Máx 5 pts
78
+ const docsCount = item.documentos?.length || 0;
79
+ if (docsCount === 0) {
80
+ score += 5;
81
+ factors.push('Sin documentos adjuntos (postulación rápida sin leer bases) (+5 pts)');
82
+ }
83
+ return {
84
+ codigo: item.codigo,
85
+ nombre: item.nombre,
86
+ organismo: item.institucion?.organismo_comprador || 'No especificado',
87
+ region: item.institucion?.nombre_region || 'No especificada',
88
+ presupuesto_disponible: budget,
89
+ ofertas_recibidas: bids,
90
+ horas_restantes: Math.round(hoursLeft * 10) / 10,
91
+ fecha_cierre: item.fechas?.fecha_cierre,
92
+ puntuacion_caliente: score,
93
+ factores_calificacion: factors,
94
+ };
95
+ }
96
+ /**
97
+ * Recolecta y rankea las oportunidades del radar.
98
+ *
99
+ * Función pura de datos (sin formato de salida): la comparten la tool
100
+ * `radar_oportunidades_calientes` (que emite JSON para el LLM) y el informe
101
+ * HTML/PDF, garantizando que ambos muestren exactamente lo mismo.
102
+ */
103
+ export async function recolectarDatosRadar(client, params, now = Date.now()) {
104
+ const maxPaginas = params.max_paginas || 3;
105
+ logger.info(`radar: escaneando procesos activos (hasta ${maxPaginas} pág.) region="${params.region || 'Todas'}" q="${params.q || 'Todos'}"`);
106
+ // Auto-paginación: escanear varias páginas para no perder oportunidades en páginas 2+
107
+ const items = await client.buscarTodo({
108
+ estado: 'publicada',
109
+ region: params.region || undefined,
110
+ q: params.q || undefined,
111
+ tamano_pagina: 50,
112
+ }, maxPaginas);
113
+ const minBudget = params.presupuesto_minimo || 0;
114
+ const opportunities = [];
115
+ for (const item of items) {
116
+ const evaluada = evaluarOportunidad(item, now, minBudget);
117
+ if (evaluada)
118
+ opportunities.push(evaluada);
119
+ }
120
+ // Ordenar descendentemente por puntuación caliente
121
+ opportunities.sort((a, b) => b.puntuacion_caliente - a.puntuacion_caliente);
122
+ const limite = params.limite_resultados || 10;
123
+ return {
124
+ oportunidades: opportunities.slice(0, limite),
125
+ totalAnalizadas: opportunities.length,
126
+ limite,
127
+ paginasEscaneadas: maxPaginas,
128
+ };
129
+ }
13
130
  export function registerRadarOportunidades(server, client) {
14
131
  server.registerTool(TOOL_NAME, {
15
132
  description: TOOL_DESCRIPTION,
16
133
  inputSchema: inputSchema,
17
134
  }, async (args) => {
18
135
  try {
19
- const queryParams = {
20
- estado: 'publicada',
21
- region: args.region || undefined,
22
- q: args.q || undefined,
23
- tamano_pagina: 50, // Obtener un lote amplio de 50 para poder rankear
24
- numero_pagina: 1,
25
- };
26
- logger.info(`radar_oportunidades_calientes: Escaneando procesos activos para region="${args.region || 'Todas'}" q="${args.q || 'Todos'}"`);
27
- const searchResponse = await client.buscar(queryParams);
28
- if (!searchResponse.items || searchResponse.items.length === 0) {
136
+ const datos = await recolectarDatosRadar(client, args);
137
+ if (datos.totalAnalizadas === 0) {
29
138
  return {
30
139
  content: [{
31
140
  type: 'text',
@@ -33,99 +142,10 @@ export function registerRadarOportunidades(server, client) {
33
142
  }],
34
143
  };
35
144
  }
36
- const now = Date.now();
37
- const opportunities = [];
38
- const minBudget = args.presupuesto_minimo || 0;
39
- for (const item of searchResponse.items) {
40
- const budget = item.montos?.monto_disponible_clp || item.montos?.monto_disponible || 0;
41
- // Filtrar por presupuesto mínimo si corresponde
42
- if (budget < minBudget) {
43
- continue;
44
- }
45
- // Calcular tiempo restante en horas
46
- let hoursLeft = 0;
47
- if (item.fechas?.fecha_cierre) {
48
- const cierreTime = new Date(item.fechas.fecha_cierre).getTime();
49
- hoursLeft = (cierreTime - now) / (1000 * 60 * 60);
50
- }
51
- // Omitir procesos que ya cerraron o están en negativo por zona horaria
52
- if (hoursLeft <= 0) {
53
- continue;
54
- }
55
- let score = 0;
56
- const factors = [];
57
- // Factor 1: Baja Competencia (total_ofertas_recibidas) - Máx 50 pts
58
- const bids = item.resumen?.total_ofertas_recibidas ?? 0;
59
- if (bids === 0) {
60
- score += 50;
61
- factors.push('Sin oferentes activos (+50 pts)');
62
- }
63
- else if (bids === 1) {
64
- score += 30;
65
- factors.push('Baja competencia: solo 1 oferente (+30 pts)');
66
- }
67
- else if (bids === 2) {
68
- score += 15;
69
- factors.push('Competencia moderada: 2 oferentes (+15 pts)');
70
- }
71
- // Factor 2: Urgencia del Cierre (horas restantes) - Máx 30 pts
72
- if (hoursLeft <= 4) {
73
- score += 30;
74
- factors.push('Urgencia crítica: cierra en menos de 4 horas (+30 pts)');
75
- }
76
- else if (hoursLeft <= 12) {
77
- score += 20;
78
- factors.push('Cierre inminente: cierra en menos de 12 horas (+20 pts)');
79
- }
80
- else if (hoursLeft <= 24) {
81
- score += 10;
82
- factors.push('Cierre cercano: cierra en menos de 24 horas (+10 pts)');
83
- }
84
- // Factor 3: Tamaño de Presupuesto - Máx 20 pts
85
- if (budget >= 5000000) {
86
- score += 20;
87
- factors.push('Presupuesto de alto valor (>= $5.000.000 CLP) (+20 pts)');
88
- }
89
- else if (budget >= 2000000) {
90
- score += 15;
91
- factors.push('Presupuesto medio-alto (>= $2.000.000 CLP) (+15 pts)');
92
- }
93
- else if (budget >= 500000) {
94
- score += 10;
95
- factors.push('Presupuesto medio (>= $500.000 CLP) (+10 pts)');
96
- }
97
- else {
98
- score += 5;
99
- factors.push('Presupuesto bajo (< $500.000 CLP) (+5 pts)');
100
- }
101
- // Factor 4: Facilidad de Postulación - Máx 5 pts
102
- const docsCount = item.documentos?.length || 0;
103
- if (docsCount === 0) {
104
- score += 5;
105
- factors.push('Sin documentos adjuntos (postulación rápida sin leer bases) (+5 pts)');
106
- }
107
- opportunities.push({
108
- codigo: item.codigo,
109
- nombre: item.nombre,
110
- organismo: item.institucion?.organismo_comprador || 'No especificado',
111
- region: item.institucion?.nombre_region || 'No especificada',
112
- presupuesto_disponible: budget,
113
- ofertas_recibidas: bids,
114
- horas_restantes: Math.round(hoursLeft * 10) / 10,
115
- fecha_cierre: item.fechas?.fecha_cierre,
116
- puntuacion_caliente: score,
117
- factores_calificacion: factors,
118
- });
119
- }
120
- // Ordenar descendentemente por puntuación caliente
121
- opportunities.sort((a, b) => b.puntuacion_caliente - a.puntuacion_caliente);
122
- // Aplicar límite
123
- const limit = args.limite_resultados || 10;
124
- const rankedOpportunities = opportunities.slice(0, limit);
125
145
  const result = {
126
- total_oportunidades_analizadas: opportunities.length,
127
- radar_limite_resultados: limit,
128
- oportunidades_calientes: rankedOpportunities,
146
+ total_oportunidades_analizadas: datos.totalAnalizadas,
147
+ radar_limite_resultados: datos.limite,
148
+ oportunidades_calientes: datos.oportunidades,
129
149
  };
130
150
  return {
131
151
  content: [{
@@ -137,7 +157,7 @@ export function registerRadarOportunidades(server, client) {
137
157
  catch (error) {
138
158
  const message = error instanceof CompraAgilApiError
139
159
  ? error.actionableMessage
140
- : `Error inesperado en radar de oportunidades: ${String(error)}`;
160
+ : `Error inesperado en radar de oportunidades: ${safeError(error)}`;
141
161
  return {
142
162
  content: [{ type: 'text', text: message }],
143
163
  isError: true,