@johpaz/hive-sdk 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -230,4 +230,4 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
230
230
 
231
231
  ---
232
232
 
233
- *Hive SDK v0.4.0 — MIT*
233
+ *Hive SDK v0.4.4 — MIT*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.4",
4
4
  "private": false,
5
5
  "description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
6
6
  "license": "MIT",
@@ -80,7 +80,7 @@
80
80
  "LICENSE"
81
81
  ],
82
82
  "engines": {
83
- "bun": ">=1.4.0"
83
+ "bun": ">=1.4.2"
84
84
  },
85
85
  "workspaces": [
86
86
  "packages/core",
@@ -115,15 +115,15 @@
115
115
  "mammoth": "^1.12.0",
116
116
  "ollama": "^0.6.3",
117
117
  "openai": "^6.18.0",
118
- "pdfjs-dist": "^5.6.205",
118
+ "pdfjs-dist": "^6.3.289",
119
119
  "pptxgenjs": "^4.0.1",
120
120
  "qrcode-terminal": "^0.12.0",
121
121
  "toon-format-parser": "^1.1.0",
122
- "xlsx": "^0.18.5",
122
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
123
123
  "zod": "^4.4.3"
124
124
  },
125
125
  "devDependencies": {
126
- "@types/bun": "^1.3.13",
126
+ "@types/bun": "^1.4.1",
127
127
  "@types/jsonwebtoken": "^9.0.10",
128
128
  "typescript": "6.0.2"
129
129
  }
@@ -12,5 +12,8 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "@types/bun": "latest"
15
+ },
16
+ "engines": {
17
+ "bun": ">=1.4.2"
15
18
  }
16
19
  }
@@ -1,3 +1,4 @@
1
+ import { resolvePort } from "../utils/port.ts";
1
2
  import * as z from "zod";
2
3
  import { mkdirSync, existsSync, readFileSync } from "node:fs";
3
4
  import * as path from "node:path";
@@ -407,7 +408,7 @@ function buildDefaultConfig(): Config {
407
408
  return {
408
409
  gateway: {
409
410
  host: process.env.HIVE_HOST || "127.0.0.1",
410
- port: parseInt(process.env.HIVE_PORT || "18790", 10),
411
+ port: resolvePort(process.env.HIVE_PORT, 18790),
411
412
  pidFile: path.join(hiveDir, "gateway.pid"),
412
413
  authToken: process.env.HIVE_AUTH_TOKEN || undefined,
413
414
  tools: {
@@ -96,11 +96,17 @@ export class SSETransport implements Transport {
96
96
  this.sessionId = sessionId;
97
97
  }
98
98
 
99
- // Track cookies for session affinity (important for n8n/proxies)
100
- const setCookie = response.headers.get("set-cookie");
101
- if (setCookie) {
102
- // Simple cookie extraction: just keep the keys and values
103
- const newCookies = setCookie.split(',').map(c => c.split(';')[0].trim());
99
+ // Track cookies for session affinity (important for n8n/proxies).
100
+ //
101
+ // `getSetCookie()` y no `get("set-cookie")`: Set-Cookie es la única cabecera
102
+ // que puede repetirse sin combinarse, y `get()` devuelve las repeticiones
103
+ // unidas con ", " (Bun 1.4 lo alineó con la spec de Fetch). Partir eso por
104
+ // coma rompe cualquier cookie cuyo valor lleve una —`Expires=Wed, 09 Jun
105
+ // 2027 10:18:14 GMT` es el caso de todos los días— y dejaba fragmentos como
106
+ // "09 Jun 2027 10:18:14 GMT" haciéndose pasar por cookies.
107
+ const setCookies = response.headers.getSetCookie();
108
+ if (setCookies.length > 0) {
109
+ const newCookies = setCookies.map(c => c.split(";")[0].trim());
104
110
  this.cookies = [...new Set([...this.cookies, ...newCookies])];
105
111
  }
106
112
  }
@@ -7,9 +7,11 @@
7
7
  * El motor de cron es propio (`./cron`), sin dependencias: sólo `setTimeout` e
8
8
  * `Intl` del runtime. Antes era `croner`.
9
9
  *
10
- * `Bun.cron()` no sirve como reemplazo —se evaluó contra el runtime 1.4.0—:
10
+ * `Bun.cron()` no sirve como reemplazo —reevaluado contra el runtime 1.4.2—:
11
11
  * acepta sólo 5 campos y rechaza el sexto, no admite una fecha ISO como patrón
12
- * (que es como se agendan los jobs `one_shot`), ignora la zona horaria, y su
12
+ * (que es como se agendan los jobs `one_shot`), no toma una zona por job (usa la
13
+ * local del proceso desde 1.4; antes era UTC, y ese cambio silencioso es
14
+ * justamente por qué no conviene delegarle la conversión), y su
13
15
  * handle no expone la próxima corrida, que es de donde sale `next_run_at` y con
14
16
  * lo que se detectan las corridas perdidas al arrancar. Tampoco tiene
15
17
  * equivalente de `protect`, `maxRuns`, `interval`, `startAt`/`stopAt` ni
@@ -8,7 +8,8 @@
8
8
  * habría sido migrar la base para no ganar nada.
9
9
  *
10
10
  * No usa `Bun.cron()`: ese sólo acepta 5 campos, no admite una fecha ISO como
11
- * patrón —que es como se agendan los jobs `one_shot`—, ignora la zona horaria y
11
+ * patrón —que es como se agendan los jobs `one_shot`—, no toma una zona por job
12
+ * (usa la local del proceso desde Bun 1.4; antes era UTC) y
12
13
  * su handle no expone la próxima corrida, que es de donde sale `next_run_at` y
13
14
  * con lo que el scheduler detecta las corridas perdidas al arrancar. Lo que sí
14
15
  * se usa de Bun es el runtime pelado: `setTimeout` e `Intl`.
@@ -2,7 +2,8 @@
2
2
  * Reloj de pared ↔ instante, en una zona horaria IANA.
3
3
  *
4
4
  * Es la parte difícil de un cron con zona horaria y la razón por la que no
5
- * alcanza con `Bun.cron.parse()`, que sólo trabaja en UTC. "Todos los días a
5
+ * alcanza con `Bun.cron.parse()`, que resuelve en una sola zona —UTC hasta Bun
6
+ * 1.3, la local del proceso desde 1.4— y nunca en la del job. "Todos los días a
6
7
  * las 9" significa las 9 **del reloj de la pared en Bogotá**, y ese instante se
7
8
  * corre una hora dos veces al año en las zonas con horario de verano. Calcular
8
9
  * el offset una sola vez y sumarlo produce un cron que se desfasa un día al año
@@ -161,7 +161,11 @@ export const SEED_DATA: SeedData = {
161
161
  // Generación actual (4.6/4.7/4.8 pasaron a "legacy"). Los IDs sin fecha ya son
162
162
  // snapshots fijos, no alias evergreen.
163
163
  { id: "claude-opus-5", providerId: "anthropic", name: "Claude Opus 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 5, outputPer1M: 25 },
164
- { id: "claude-sonnet-5", providerId: "anthropic", name: "Claude Sonnet 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 3, outputPer1M: 15 },
164
+ { id: "claude-sonnet-5", providerId: "anthropic", name: "Claude Sonnet 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
165
+ // Fable 5.1 sucede a Fable 5 en el mismo escalón y al mismo precio: es el
166
+ // modelo más capaz de Anthropic con disponibilidad general. Fable 5 sigue
167
+ // servido, así que quedan los dos.
168
+ { id: "claude-fable-5-1", providerId: "anthropic", name: "Claude Fable 5.1", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
165
169
  { id: "claude-fable-5", providerId: "anthropic", name: "Claude Fable 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
166
170
  { id: "claude-haiku-4-5-20251001", providerId: "anthropic", name: "Claude Haiku 4.5", modelType: "llm", contextWindow: 200000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1, outputPer1M: 5 },
167
171
 
@@ -182,6 +186,10 @@ export const SEED_DATA: SeedData = {
182
186
  // Solo la generación 3.x: la familia 2.0 ya está apagada y la 2.5 quedó
183
187
  // superada. `gemini-3.5-pro` y `gemini-3.1-flash-lite-preview` se quitaron:
184
188
  // el primero no existe en el catálogo y el segundo ya salió de preview.
189
+ // Precio de lanzamiento hasta el 2026-12-31; el 2027-01-01 sube a 1.5/7.5,
190
+ // que es la tarifa con la que ya está sembrado gemini-3.6-flash.
191
+ { id: "gemini-3.8-flash", providerId: "gemini", name: "Gemini 3.8 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
192
+ { id: "gemini-3.7-flash", providerId: "gemini", name: "Gemini 3.7 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
185
193
  { id: "gemini-3.6-flash", providerId: "gemini", name: "Gemini 3.6 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 7.5 },
186
194
  { id: "gemini-3.5-flash", providerId: "gemini", name: "Gemini 3.5 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 9 },
187
195
  { id: "gemini-3.5-flash-lite", providerId: "gemini", name: "Gemini 3.5 Flash Lite", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.3, outputPer1M: 2.5 },
@@ -214,6 +222,10 @@ export const SEED_DATA: SeedData = {
214
222
  // 384K de salida máxima y tool calling en ambos.
215
223
  { id: "deepseek-v4-pro", providerId: "deepseek", name: "DeepSeek V4 Pro", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.435, outputPer1M: 0.87 },
216
224
  { id: "deepseek-v4-flash", providerId: "deepseek", name: "DeepSeek V4 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.28 },
225
+ // Variante experimental de V4 Flash con entrada de imagen; DeepSeek la tarifa
226
+ // igual que la base, por eso repite precio. Las imágenes se facturan como
227
+ // entrada, hasta 384 tokens cada una sin importar la resolución.
228
+ { id: "deepseek-v4-flash-vision-exp", providerId: "deepseek", name: "DeepSeek V4 Flash Vision (exp)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.28 },
217
229
 
218
230
  // ── Kimi / Moonshot (fuente: platform.kimi.ai/docs/pricing/chat) ──
219
231
  // La serie moonshot-v1-* se apaga el 2026-08-31; K2/K2.5 quedaron superadas.
@@ -223,32 +235,42 @@ export const SEED_DATA: SeedData = {
223
235
 
224
236
  // ── OpenRouter (fuente: GET https://openrouter.ai/api/v1/models) ──
225
237
  // Solo modelos vivos con `tools` en supported_parameters y publicados desde
226
- // 2025-07. contextWindow = context_length reportado por el propio catálogo.
238
+ // 2025-07. contextWindow = context_length reportado por el propio catálogo,
239
+ // y los precios también salen de ahí — son los de la ruta de OpenRouter, no
240
+ // los del proveedor nativo, y se mueven solos. Revalidado el 2026-09-03.
227
241
  // Anthropic
242
+ { id: "anthropic/claude-fable-5.1", providerId: "openrouter", name: "Claude Fable 5.1 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
228
243
  { id: "anthropic/claude-opus-5", providerId: "openrouter", name: "Claude Opus 5 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 5, outputPer1M: 25 },
229
244
  { id: "anthropic/claude-sonnet-5", providerId: "openrouter", name: "Claude Sonnet 5 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
230
245
  // OpenAI — la serie 5.6 se divide en Sol (flagship), Terra (equilibrado) y Luna (económico)
231
- { id: "openai/gpt-5.6-sol", providerId: "openrouter", name: "GPT-5.6 Sol (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 5, outputPer1M: 30 },
232
- { id: "openai/gpt-5.6-terra", providerId: "openrouter", name: "GPT-5.6 Terra (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 1, outputPer1M: 6 },
233
- { id: "openai/gpt-5.6-luna", providerId: "openrouter", name: "GPT-5.6 Luna (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.1, outputPer1M: 0.6 },
246
+ { id: "openai/gpt-5.6-sol", providerId: "openrouter", name: "GPT-5.6 Sol (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
247
+ { id: "openai/gpt-5.6-terra", providerId: "openrouter", name: "GPT-5.6 Terra (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 2, outputPer1M: 12 },
248
+ { id: "openai/gpt-5.6-luna", providerId: "openrouter", name: "GPT-5.6 Luna (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.2, outputPer1M: 1.2 },
234
249
  // Google
235
- { id: "google/gemini-3.6-flash", providerId: "openrouter", name: "Gemini 3.6 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 7.5 },
250
+ { id: "google/gemini-3.8-flash", providerId: "openrouter", name: "Gemini 3.8 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
251
+ { id: "google/gemini-3.7-flash", providerId: "openrouter", name: "Gemini 3.7 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
252
+ { id: "google/gemini-3.6-flash", providerId: "openrouter", name: "Gemini 3.6 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
236
253
  { id: "google/gemini-3.5-flash", providerId: "openrouter", name: "Gemini 3.5 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 9 },
237
254
  { id: "google/gemini-3.1-pro-preview", providerId: "openrouter", name: "Gemini 3.1 Pro (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 12 },
238
255
  // DeepSeek
239
- { id: "deepseek/deepseek-v4-pro", providerId: "openrouter", name: "DeepSeek V4 Pro (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.435, outputPer1M: 0.87 },
240
- { id: "deepseek/deepseek-v4-flash", providerId: "openrouter", name: "DeepSeek V4 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.14, outputPer1M: 0.28 },
256
+ { id: "deepseek/deepseek-v4-flash-vision-exp", providerId: "openrouter", name: "DeepSeek V4 Flash Vision exp (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.44, outputPer1M: 1.32 },
257
+ { id: "deepseek/deepseek-v4-pro", providerId: "openrouter", name: "DeepSeek V4 Pro (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 1.04226, outputPer1M: 2.08452 },
258
+ { id: "deepseek/deepseek-v4-flash", providerId: "openrouter", name: "DeepSeek V4 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.088606, outputPer1M: 0.177212 },
241
259
  // Kimi
242
260
  { id: "moonshotai/kimi-k3", providerId: "openrouter", name: "Kimi K3 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 3, outputPer1M: 15 },
243
- { id: "moonshotai/kimi-k2.7-code", providerId: "openrouter", name: "Kimi K2.7 Code (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.73, outputPer1M: 3.5 },
261
+ { id: "moonshotai/kimi-k2.7-code", providerId: "openrouter", name: "Kimi K2.7 Code (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.66, outputPer1M: 3.4 },
244
262
  // MiniMax
245
263
  { id: "minimax/minimax-m3", providerId: "openrouter", name: "MiniMax M3 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.3, outputPer1M: 1.2 },
246
264
  // Z.ai / GLM
247
- { id: "z-ai/glm-5.2", providerId: "openrouter", name: "GLM 5.2 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.63, outputPer1M: 1.98 },
265
+ { id: "z-ai/glm-5.3", providerId: "openrouter", name: "GLM 5.3 (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 1.4, outputPer1M: 4.4 },
266
+ { id: "z-ai/glm-5.3-flash", providerId: "openrouter", name: "GLM 5.3 Flash (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
267
+ { id: "z-ai/glm-5.2", providerId: "openrouter", name: "GLM 5.2 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.966, outputPer1M: 3.036 },
248
268
  // Qwen
249
269
  { id: "qwen/qwen3.8-max", providerId: "openrouter", name: "Qwen3.8 Max (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
270
+ { id: "qwen/qwen3.8-flash", providerId: "openrouter", name: "Qwen3.8 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.15, outputPer1M: 0.47 },
250
271
  { id: "qwen/qwen3.7-flash", providerId: "openrouter", name: "Qwen3.7 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.03, outputPer1M: 0.13 },
251
272
  // xAI
273
+ { id: "x-ai/grok-4.6", providerId: "openrouter", name: "Grok 4.6 (OR)", modelType: "llm", contextWindow: 500000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
252
274
  { id: "x-ai/grok-4.5", providerId: "openrouter", name: "Grok 4.5 (OR)", modelType: "llm", contextWindow: 500000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
253
275
  // Mistral
254
276
  { id: "mistralai/mistral-medium-3-5", providerId: "openrouter", name: "Mistral Medium 3.5 (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 1.5, outputPer1M: 7.5 },
@@ -283,9 +305,14 @@ export const SEED_DATA: SeedData = {
283
305
  { id: "eleven_v3", providerId: "elevenlabs", name: "Eleven V3", modelType: "tts", contextWindow: 0, capabilities: JSON.stringify(["tts", "speech", "expressive"]) },
284
306
 
285
307
  // ── Qwen (Alibaba DashScope / Model Studio) ──
286
- // Serie 3.7 = generación actual. Los contextWindow salen del catálogo de
287
- // OpenRouter, que enruta a los mismos modelos: el `qwen3.6-max-preview` que
288
- // estaba sembrado con 32768 en realidad tiene 262144.
308
+ // Serie 3.8 = generación actual; la 3.7 sigue servida. Los contextWindow salen
309
+ // del catálogo de OpenRouter, que enruta a los mismos modelos: el
310
+ // `qwen3.6-max-preview` que estaba sembrado con 32768 en realidad tiene 262144.
311
+ // Serie 3.8: Max es el buque insignia y Flash el de alto volumen. Precios del
312
+ // endpoint internacional (Singapur), que es el `baseUrl` sembrado — el de
313
+ // China continental cobra entre 60% y 70% menos.
314
+ { id: "qwen3.8-max", providerId: "qwen", name: "Qwen 3.8 Max", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
315
+ { id: "qwen3.8-flash", providerId: "qwen", name: "Qwen 3.8 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.42 },
289
316
  { id: "qwen3.7-max", providerId: "qwen", name: "Qwen 3.7 Max", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.475, outputPer1M: 4.425 },
290
317
  { id: "qwen3.7-plus", providerId: "qwen", name: "Qwen 3.7 Plus", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.32, outputPer1M: 1.28 },
291
318
  { id: "qwen3.6-flash", providerId: "qwen", name: "Qwen 3.6 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.1875, outputPer1M: 1.125 },
@@ -300,10 +327,13 @@ export const SEED_DATA: SeedData = {
300
327
  // Solo los mejores modelos agénticos (tool calling) del catálogo vivo. NVIDIA
301
328
  // retira modelos del endpoint sin avisar y responde 410 Gone al llamarlos, así
302
329
  // que esta lista se valida contra /v1/models — no contra la web de build.nvidia.com,
303
- // que sigue mostrando fichas de modelos ya retirados. Verificado 2026-08-11.
330
+ // que sigue mostrando fichas de modelos ya retirados. Verificado 2026-09-03.
304
331
  // Nota: Qwen ya no tiene ningún modelo en el catálogo NVIDIA (todos retirados);
305
332
  // para Qwen usar el provider `qwen` (DashScope) directamente.
306
- { id: "z-ai/glm-5.2", providerId: "nvidia", name: "GLM 5.2 (NVIDIA)", modelType: "llm", contextWindow: 200000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
333
+ // z-ai/glm-5.2 se sacó: NVIDIA lo retiró de /v1/models. Para GLM usar el
334
+ // provider `z-ai` directo, o el enrutado de OpenRouter.
335
+ { id: "moonshotai/kimi-k3", providerId: "nvidia", name: "Kimi K3 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
336
+ { id: "nvidia/nemotron-3.5-lightning-30b-a3b", providerId: "nvidia", name: "Nemotron 3.5 Lightning 30B", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
307
337
  { id: "moonshotai/kimi-k2.6", providerId: "nvidia", name: "Kimi K2.6 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
308
338
  { id: "minimaxai/minimax-m3", providerId: "nvidia", name: "MiniMax M3 (NVIDIA)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
309
339
  { id: "nvidia/nemotron-3-ultra-550b-a55b", providerId: "nvidia", name: "Nemotron 3 Ultra 550B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
@@ -325,6 +355,7 @@ export const SEED_DATA: SeedData = {
325
355
  { id: "Qwen-Ambassador/Qwen3.7-Max", providerId: "modelscope", name: "Qwen3.7 Max (Embajador)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
326
356
  { id: "Qwen-Ambassador/Qwen3.7-Plus", providerId: "modelscope", name: "Qwen3.7 Plus (Embajador)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
327
357
  // Open-weight del mismo endpoint, para cuentas sin permiso de embajador.
358
+ { id: "Qwen/Qwen3.8-27B", providerId: "modelscope", name: "Qwen3.8 27B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
328
359
  { id: "Qwen/Qwen3.5-397B-A17B", providerId: "modelscope", name: "Qwen3.5 397B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
329
360
  { id: "Qwen/Qwen3-Next-80B-A3B-Instruct", providerId: "modelscope", name: "Qwen3 Next 80B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
330
361
  { id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", providerId: "modelscope", name: "Qwen3 Coder 30B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
@@ -338,11 +369,30 @@ export const SEED_DATA: SeedData = {
338
369
  { id: "MiniMax-M2.7-highspeed", providerId: "minimax", name: "MiniMax M2.7 Highspeed", modelType: "llm", contextWindow: 204800, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0.3, outputPer1M: 1.2 },
339
370
 
340
371
  // ── Z.ai / GLM (fuente: docs.z.ai/guides/llm) — OpenAI-compatible endpoint ──
372
+ // GLM-5.3 es sólo texto (código y ciberseguridad); el Flash es el multimodal
373
+ // barato. OJO: 0.075/0.25 del Flash es promoción a mitad de precio hasta el
374
+ // 2026-09-09 (24:00 UTC+8); la tarifa de lista es 0.15/0.50.
375
+ { id: "glm-5.3", providerId: "z-ai", name: "GLM 5.3", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.4, outputPer1M: 4.4 },
376
+ { id: "glm-5.3-flash", providerId: "z-ai", name: "GLM 5.3 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
341
377
  { id: "glm-5.2", providerId: "z-ai", name: "GLM 5.2", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.63, outputPer1M: 1.98 },
342
378
  { id: "glm-5.1", providerId: "z-ai", name: "GLM 5.1", modelType: "llm", contextWindow: 204800, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.97, outputPer1M: 3.04 },
343
379
  { id: "glm-5", providerId: "z-ai", name: "GLM 5", modelType: "llm", contextWindow: 200000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.97, outputPer1M: 3.04 },
344
380
 
345
- // ── OpenCode Go (fuente: opencode.ai) — OpenAI-compatible endpoint ──
381
+ // ── OpenCode Go (fuente: GET https://opencode.ai/zen/go/v1/models) ──
382
+ // Endpoint gratuito y OpenAI-compatible, por eso todo va con precio 0. El
383
+ // contextWindow es conservador a propósito: el listado no publica el ctx
384
+ // servido, y quedarse corto sólo adelanta la compactación mientras que
385
+ // pasarse revienta la llamada. Verificado 2026-09-03.
386
+ { id: "glm-5.3", providerId: "opencode-go", name: "GLM-5.3", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
387
+ { id: "glm-5.3-flash", providerId: "opencode-go", name: "GLM-5.3 Flash", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
388
+ { id: "glm-5.2", providerId: "opencode-go", name: "GLM-5.2", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
389
+ { id: "kimi-k3", providerId: "opencode-go", name: "Kimi K3", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
390
+ { id: "kimi-k2.7-code", providerId: "opencode-go", name: "Kimi K2.7 Code", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
391
+ { id: "qwen3.8-max", providerId: "opencode-go", name: "Qwen3.8 Max", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
392
+ { id: "qwen3.8-flash", providerId: "opencode-go", name: "Qwen3.8 Flash", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
393
+ { id: "grok-4.6", providerId: "opencode-go", name: "Grok 4.6", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
394
+ { id: "deepseek-v4-flash-vision-exp", providerId: "opencode-go", name: "DeepSeek V4 Flash Vision (exp)", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
395
+ { id: "hy4-preview", providerId: "opencode-go", name: "Hunyuan 4 Preview", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
346
396
  { id: "minimax-m3", providerId: "opencode-go", name: "MiniMax M3", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
347
397
  { id: "minimax-m2.7", providerId: "opencode-go", name: "MiniMax M2.7", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
348
398
  { id: "minimax-m2.5", providerId: "opencode-go", name: "MiniMax M2.5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
@@ -10,6 +10,14 @@ import type { Tool } from "../types.ts";
10
10
  import { logger } from "../../utils/logger.ts";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import {
15
+ assertBeforeDeadline,
16
+ MAX_PDF_INPUT_BYTES,
17
+ MAX_PDF_PAGES_PER_REQUEST,
18
+ OFFICE_PROCESSING_TIMEOUT_MS,
19
+ validateOfficeInput,
20
+ } from "./security-limits.ts";
13
21
 
14
22
  const log = logger.child("office-leer-pdf");
15
23
 
@@ -48,61 +56,102 @@ export const officeLeerPdfTool: Tool = {
48
56
  return { ok: false, error: `Archivo no encontrado: ${rutaAbsoluta}` };
49
57
  }
50
58
 
59
+ const inputError = validateOfficeInput(
60
+ rutaAbsoluta,
61
+ MAX_PDF_INPUT_BYTES,
62
+ "El PDF",
63
+ );
64
+ if (inputError) return { ok: false, error: inputError };
65
+
51
66
  const buffer = fs.readFileSync(rutaAbsoluta);
52
67
  const uint8Array = new Uint8Array(buffer);
53
68
 
54
- // Importar pdfjs-dist (compatible con Bun, sin worker)
69
+ // La build legacy conserva compatibilidad con Bun. PDF.js configura su
70
+ // propio fake worker en Node/Bun; borrar workerSrc rompe PDF.js 6+.
55
71
  const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs" as any).catch(
56
72
  () => import("pdfjs-dist" as any)
57
73
  );
58
74
 
59
- // Desactivar worker para entorno Node/Bun
60
75
  const lib = pdfjsLib.default ?? pdfjsLib;
61
- if (lib.GlobalWorkerOptions) {
62
- lib.GlobalWorkerOptions.workerSrc = "";
63
- }
64
-
65
- const doc = await lib.getDocument({ data: uint8Array, disableWorker: true }).promise;
66
- const totalPaginas = doc.numPages;
76
+ const pdfjsPackageUrl = import.meta.resolve("pdfjs-dist/package.json");
77
+ const loadingTask = lib.getDocument({
78
+ data: uint8Array,
79
+ enableScripting: false,
80
+ isEvalSupported: false,
81
+ // Bun's process.getBuiltinModule("fs/promises") expects a filesystem
82
+ // path here; a file:// URL string is not accepted.
83
+ standardFontDataUrl: fileURLToPath(
84
+ new URL("./standard_fonts/", pdfjsPackageUrl),
85
+ ),
86
+ });
67
87
 
68
- // Metadata
69
- let titulo: string | undefined;
70
88
  try {
71
- const meta = await doc.getMetadata();
72
- titulo = (meta?.info as any)?.Title ?? undefined;
73
- } catch {
74
- // metadata opcional
89
+ const deadline = Date.now() + OFFICE_PROCESSING_TIMEOUT_MS;
90
+ const doc = await loadingTask.promise;
91
+ const totalPaginas = doc.numPages;
92
+
93
+ // Metadata
94
+ let titulo: string | undefined;
95
+ try {
96
+ const meta = await doc.getMetadata();
97
+ titulo = (meta?.info as any)?.Title ?? undefined;
98
+ } catch {
99
+ // metadata opcional
100
+ }
101
+
102
+ const inicio = paginaInicio;
103
+ if (!Number.isInteger(inicio) || inicio > totalPaginas) {
104
+ return {
105
+ ok: false,
106
+ error: `La página inicial debe estar entre 1 y ${totalPaginas}`,
107
+ };
108
+ }
109
+ if (paginaFin !== undefined && (!Number.isInteger(paginaFin) || paginaFin < inicio)) {
110
+ return {
111
+ ok: false,
112
+ error: "La página final debe ser un entero mayor o igual a la página inicial",
113
+ };
114
+ }
115
+
116
+ const fin = paginaFin ? Math.min(paginaFin, totalPaginas) : totalPaginas;
117
+ const paginasSolicitadas = fin - inicio + 1;
118
+ if (paginasSolicitadas > MAX_PDF_PAGES_PER_REQUEST) {
119
+ return {
120
+ ok: false,
121
+ error: `Se pueden leer como máximo ${MAX_PDF_PAGES_PER_REQUEST} páginas por solicitud`,
122
+ };
123
+ }
124
+
125
+ const textosPorPagina: Array<{ pagina: number; texto: string }> = [];
126
+
127
+ for (let i = inicio; i <= fin; i++) {
128
+ assertBeforeDeadline(deadline, "La lectura del PDF");
129
+ const pagina = await doc.getPage(i);
130
+ const contenido = await pagina.getTextContent();
131
+ const texto = (contenido.items as any[])
132
+ .map((item: any) => item.str ?? "")
133
+ .join(" ")
134
+ .replace(/\s+/g, " ")
135
+ .trim();
136
+ textosPorPagina.push({ pagina: i, texto });
137
+ }
138
+
139
+ const textoCompleto = textosPorPagina.map((p) => p.texto).join("\n\n");
140
+
141
+ log.info(`PDF leído: ${totalPaginas} páginas, ${textoCompleto.length} caracteres`);
142
+
143
+ return {
144
+ ok: true,
145
+ ruta: rutaAbsoluta,
146
+ totalPaginas,
147
+ paginasLeidas: paginasSolicitadas,
148
+ titulo,
149
+ texto: textoCompleto,
150
+ paginas: textosPorPagina,
151
+ };
152
+ } finally {
153
+ await loadingTask.destroy();
75
154
  }
76
-
77
- const inicio = paginaInicio;
78
- const fin = paginaFin ? Math.min(paginaFin, totalPaginas) : totalPaginas;
79
-
80
- const textosPorPagina: Array<{ pagina: number; texto: string }> = [];
81
-
82
- for (let i = inicio; i <= fin; i++) {
83
- const pagina = await doc.getPage(i);
84
- const contenido = await pagina.getTextContent();
85
- const texto = (contenido.items as any[])
86
- .map((item: any) => item.str ?? "")
87
- .join(" ")
88
- .replace(/\s+/g, " ")
89
- .trim();
90
- textosPorPagina.push({ pagina: i, texto });
91
- }
92
-
93
- const textoCompleto = textosPorPagina.map((p) => p.texto).join("\n\n");
94
-
95
- log.info(`PDF leído: ${totalPaginas} páginas, ${textoCompleto.length} caracteres`);
96
-
97
- return {
98
- ok: true,
99
- ruta: rutaAbsoluta,
100
- totalPaginas,
101
- paginasLeidas: fin - inicio + 1,
102
- titulo,
103
- texto: textoCompleto,
104
- paginas: textosPorPagina,
105
- };
106
155
  } catch (error) {
107
156
  log.error(`Error leyendo PDF: ${(error as Error).message}`);
108
157
  return {
@@ -11,6 +11,14 @@ import { logger } from "../../utils/logger.ts";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { cargarXlsx } from "./xlsx-loader.ts";
14
+ import {
15
+ assertBeforeDeadline,
16
+ MAX_XLSX_INPUT_BYTES,
17
+ MAX_XLSX_ROWS_PER_SHEET,
18
+ MAX_XLSX_SHEETS,
19
+ OFFICE_PROCESSING_TIMEOUT_MS,
20
+ validateOfficeInput,
21
+ } from "./security-limits.ts";
14
22
 
15
23
  const log = logger.child("office-leer-xlsx");
16
24
 
@@ -56,32 +64,43 @@ export const officeLeerXlsxTool: Tool = {
56
64
  return { ok: false, error: `Archivo no encontrado: ${rutaAbsoluta}` };
57
65
  }
58
66
 
67
+ const inputError = validateOfficeInput(
68
+ rutaAbsoluta,
69
+ MAX_XLSX_INPUT_BYTES,
70
+ "El XLSX",
71
+ );
72
+ if (inputError) return { ok: false, error: inputError };
73
+
59
74
  const XLSX = await cargarXlsx();
60
75
  const buffer = fs.readFileSync(rutaAbsoluta);
61
- const workbook = XLSX.read(buffer, { type: "buffer" });
76
+ const deadline = Date.now() + OFFICE_PROCESSING_TIMEOUT_MS;
77
+ const workbook = XLSX.read(buffer, {
78
+ type: "buffer",
79
+ sheetRows: MAX_XLSX_ROWS_PER_SHEET + 2,
80
+ sheets: hojaFiltro,
81
+ });
62
82
 
63
83
  const nombresHojas = hojaFiltro
64
84
  ? [hojaFiltro]
65
85
  : workbook.SheetNames;
66
86
 
87
+ if (nombresHojas.length > MAX_XLSX_SHEETS) {
88
+ return {
89
+ ok: false,
90
+ error: `El XLSX contiene más de ${MAX_XLSX_SHEETS} hojas`,
91
+ };
92
+ }
93
+
67
94
  const hojas: Record<string, any[]> = {};
68
95
 
69
96
  for (const nombreHoja of nombresHojas) {
97
+ assertBeforeDeadline(deadline, "La lectura del XLSX");
70
98
  const hoja = workbook.Sheets[nombreHoja];
71
99
  if (!hoja) {
72
100
  log.warn(`Hoja '${nombreHoja}' no encontrada en el archivo`);
73
101
  continue;
74
102
  }
75
103
 
76
- const opciones: any = {
77
- header: incluirEncabezados ? 1 : 1,
78
- defval: "",
79
- };
80
-
81
- if (rango) {
82
- opciones.range = rango;
83
- }
84
-
85
104
  if (incluirEncabezados) {
86
105
  // La primera fila se usa como encabezados
87
106
  hojas[nombreHoja] = XLSX.utils.sheet_to_json(hoja, {
@@ -96,6 +115,13 @@ export const officeLeerXlsxTool: Tool = {
96
115
  range: rango,
97
116
  });
98
117
  }
118
+
119
+ if (hojas[nombreHoja].length > MAX_XLSX_ROWS_PER_SHEET) {
120
+ return {
121
+ ok: false,
122
+ error: `La hoja '${nombreHoja}' supera el máximo de 10.000 filas`,
123
+ };
124
+ }
99
125
  }
100
126
 
101
127
  const totalFilas = Object.values(hojas).reduce(
@@ -0,0 +1,28 @@
1
+ import { statSync } from "node:fs";
2
+
3
+ export const MAX_PDF_INPUT_BYTES = 25 * 1024 * 1024;
4
+ export const MAX_XLSX_INPUT_BYTES = 15 * 1024 * 1024;
5
+ export const MAX_PDF_PAGES_PER_REQUEST = 200;
6
+ export const MAX_XLSX_SHEETS = 50;
7
+ export const MAX_XLSX_ROWS_PER_SHEET = 10_000;
8
+ export const OFFICE_PROCESSING_TIMEOUT_MS = 30_000;
9
+
10
+ export function validateOfficeInput(
11
+ filePath: string,
12
+ maxBytes: number,
13
+ label: string,
14
+ ): string | null {
15
+ const stat = statSync(filePath);
16
+ if (!stat.isFile()) return `${label} debe ser un archivo regular`;
17
+ if (stat.size > maxBytes) {
18
+ const maxMiB = maxBytes / (1024 * 1024);
19
+ return `${label} excede el límite de ${maxMiB} MiB`;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export function assertBeforeDeadline(deadline: number, label: string): void {
25
+ if (Date.now() > deadline) {
26
+ throw new Error(`${label} excedió el límite de procesamiento de 30 segundos`);
27
+ }
28
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Resolución de puertos desde el entorno.
3
+ *
4
+ * Bun 1.4 endureció `Bun.serve`: un puerto fuera de `[0, 65535]` —o `NaN`, que
5
+ * es lo que devuelve `parseInt("no-es-un-numero")`— ahora lanza `RangeError` en
6
+ * vez de recortar el valor. Un `HIVE_PORT` mal escrito dejó de degradar y pasó a
7
+ * tumbar el arranque con una excepción sin capturar:
8
+ *
9
+ * RangeError: The value of "options.port" is out of range.
10
+ * It must be an integer. Received NaN
11
+ *
12
+ * Este helper vuelve a la degradación explícita: avisa y sigue con el puerto por
13
+ * defecto, que para una variable de entorno mal tipeada es el comportamiento
14
+ * útil. El `0` se deja pasar a propósito: `Bun.serve` lo interpreta como "asigná
15
+ * un puerto libre" y hay código que se apoya en eso.
16
+ *
17
+ * Avisa con `console.warn` y no con el logger del proyecto a propósito: esto lo
18
+ * usa `config/loader.ts`, y el logger importa `config/loader.ts` para saber
19
+ * dónde escribir. Importarlo acá cierra el ciclo y rompe con "Cannot access
20
+ * 'logger' before initialization". Además, cuando se resuelve un puerto el
21
+ * logger todavía no está configurado.
22
+ */
23
+
24
+ export function resolvePort(raw: unknown, fallback: number): number {
25
+ if (raw === undefined || raw === null || raw === "") return fallback
26
+
27
+ const n = typeof raw === "number" ? raw : Number(String(raw).trim())
28
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
29
+ console.warn(`[config] Puerto inválido ${JSON.stringify(raw)}; se usa ${fallback}`)
30
+ return fallback
31
+ }
32
+ return n
33
+ }
@@ -1,3 +1,4 @@
1
+ import { resolvePort } from "../utils/port.ts";
1
2
  import { col } from "../storage/hive.ts";
2
3
  import type { ChannelDoc, ModelDoc } from "../storage/collections.ts";
3
4
  import { loadProviderApiKey } from "../storage/crypto.ts";
@@ -281,7 +282,7 @@ class VoiceService {
281
282
 
282
283
  private async speakWithPiper(text: string, voiceId?: string): Promise<AudioOutput> {
283
284
  const cleanText = cleanTextForTTS(text);
284
- const port = Number(process.env.TTS_PORT ?? 5500);
285
+ const port = resolvePort(process.env.TTS_PORT, 5500);
285
286
  const res = await fetch(`http://localhost:${port}/tts`, {
286
287
  method: "POST",
287
288
  headers: { "Content-Type": "application/json" },