visualizar-rate-limit 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # visualizar-r-l
2
+
3
+ (banner_visualizar.png)
4
+
5
+ Una CLI liviana para ver los rate limits de Codex sin salir de la terminal. Si eres de los que revisan su uso con `tmux`, `weechat` o cualquier terminal de la vieja escuela, este mini panel te da dos barras limpias y un `watch` para mantener el pulso del consumo.
6
+
7
+ ## Qué se ve
8
+
9
+ (example.png)
10
+
11
+ 1. **Dos ventanas**: carga de 5h y semanal con porcentajes coloreados (verde/amarillo/rojo según urgencia).
12
+ 2. **Reset automático**: el parser extrae etiquetas como `Reset:` y muestra cuándo se renueva cada cuota.
13
+ 3. **Modo `watch`**: refresca el panel cada `45s` y usa la sesión de Chrome que ya tienes abierta para evitar bloqueos anti-bot.
14
+
15
+ De momento solo funciona con `chatgpt.com/codex/settings/usage`. Próximamente integraré otras plataformas (Anthropic, Gemini, etc.) manteniendo el mismo enfoque simple y terminal-first.
16
+
17
+ ## Instalación
18
+
19
+ ### Desde npm
20
+
21
+ ```bash
22
+ npm install -g visualizar-rate-limit
23
+ ```
24
+
25
+ ### Desde git
26
+
27
+ ```bash
28
+ git clone git@github.com:NicBJ/visualizar-rate-limit.git
29
+ cd visualizar-rate-limit
30
+ ./scripts/install.sh
31
+ ```
32
+
33
+ ## Flujo básico
34
+
35
+ 1. `visualizar-r-l login`: abre Chrome en background con DevTools remoto y deja la página de usage cargada.
36
+ 2. `visualizar-r-l`: muestra las barras una vez.
37
+ 3. `visualizar-r-l watch`: monitorea cada 45 segundos.
38
+
39
+ Rodea `watch` con `VRL_REFRESH_SECONDS=15` si necesitas un refresco más agresivo.
40
+
41
+ ## Comandos útiles
42
+
43
+ ```
44
+ visualizar-r-l login
45
+ visualizar-r-l status
46
+ visualizar-r-l stop
47
+ visualizar-r-l watch
48
+ visualizar-r-l raw
49
+ visualizar-r-l debug
50
+ visualizar-r-l doctor
51
+ ```
52
+
53
+ `status` y `stop` controlan la sesión en background; `doctor` verifica dependencias; `debug` imprime trazas en caso de parsing extraño.
54
+
55
+ ## Personalización
56
+
57
+ - `CHROME_BIN`: forzar un binario distinto.
58
+ - `VRL_PROFILE_DIR`: guarda sesiones en otra carpeta.
59
+ - `VRL_DEBUG_PORT`: cambia el puerto DevTools si 9223 está en uso.
60
+ - `VRL_REFRESH_SECONDS`: intervalo del modo `watch`.
61
+
62
+ ## Próximos pasos
63
+
64
+ 1. Añadir soporte para Anthropic y Gemini (manteniendo extractor basado en sesión real).
65
+ 2. Agregar tests que validen el parser con HTML de ejemplo.
66
+ 3. Publicar releases en npm y crear un `CHANGELOG.md`.
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ APP_NAME="visualizar-r-l"
6
+ URL="https://chatgpt.com/codex/settings/usage"
7
+ SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
8
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
9
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10
+ DEFAULT_STATE_ROOT="${XDG_STATE_HOME:-$HOME/.local/state}/${APP_NAME}"
11
+ STATE_ROOT="${VRL_STATE_ROOT:-$DEFAULT_STATE_ROOT}"
12
+ REFRESH_SECONDS="${VRL_REFRESH_SECONDS:-45}"
13
+ DEBUG_PORT="${VRL_DEBUG_PORT:-9223}"
14
+
15
+ ensure_state_root() {
16
+ if mkdir -p "$STATE_ROOT" 2>/dev/null; then
17
+ return 0
18
+ fi
19
+
20
+ STATE_ROOT="$SCRIPT_DIR/.state"
21
+ mkdir -p "$STATE_ROOT"
22
+ }
23
+
24
+ ensure_state_root
25
+ PROFILE_DIR="${VRL_PROFILE_DIR:-$STATE_ROOT/chrome-profile}"
26
+ PID_FILE="$STATE_ROOT/chrome.pid"
27
+ LOG_FILE="$STATE_ROOT/chrome.log"
28
+ mkdir -p "$PROFILE_DIR"
29
+
30
+ die() {
31
+ printf 'Error: %s\n' "$*" >&2
32
+ exit 1
33
+ }
34
+
35
+ find_browser() {
36
+ local candidate
37
+ for candidate in \
38
+ "${CHROME_BIN:-}" \
39
+ google-chrome \
40
+ google-chrome-stable \
41
+ chromium-browser \
42
+ chromium
43
+ do
44
+ if [[ -n "$candidate" ]] && command -v "$candidate" >/dev/null 2>&1; then
45
+ command -v "$candidate"
46
+ return 0
47
+ fi
48
+ done
49
+
50
+ return 1
51
+ }
52
+
53
+ usage() {
54
+ cat <<'EOF'
55
+ Uso:
56
+ visualizar-r-l Muestra las dos barras una vez
57
+ visualizar-r-l watch Refresca continuamente
58
+ visualizar-r-l login Abre Chrome en background con perfil persistente
59
+ visualizar-r-l status Muestra si la sesión en background está activa
60
+ visualizar-r-l stop Cierra la sesión en background
61
+ visualizar-r-l doctor Verifica dependencias y estado local
62
+ visualizar-r-l raw Imprime el texto extraído de la página para depuración
63
+ visualizar-r-l debug Igual que raw, pero con trazas de ejecución
64
+ visualizar-r-l help Muestra esta ayuda
65
+
66
+ Variables opcionales:
67
+ CHROME_BIN Ruta al binario de Chrome/Chromium
68
+ VRL_PROFILE_DIR Directorio de perfil persistente
69
+ VRL_REFRESH_SECONDS Segundos entre refresh en modo watch
70
+ VRL_DEBUG_PORT Puerto de DevTools para la sesión visible
71
+ EOF
72
+ }
73
+
74
+ print_kv() {
75
+ printf '%-14s %s\n' "$1" "$2"
76
+ }
77
+
78
+ is_login_running() {
79
+ if [[ ! -f "$PID_FILE" ]]; then
80
+ return 1
81
+ fi
82
+
83
+ local pid
84
+ pid="$(cat "$PID_FILE" 2>/dev/null || true)"
85
+ [[ -n "$pid" ]] || return 1
86
+ kill -0 "$pid" 2>/dev/null
87
+ }
88
+
89
+ run_helper() {
90
+ local mode="$1"
91
+ local debug_flag="${2:-}"
92
+ local browser
93
+ browser="$(find_browser)" || die "No encontré Chrome/Chromium. Define CHROME_BIN."
94
+
95
+ CHROME_BIN="$browser" \
96
+ VRL_PROFILE_DIR="$PROFILE_DIR" \
97
+ VRL_URL="$URL" \
98
+ VRL_DEBUG_PORT="$DEBUG_PORT" \
99
+ node "$PROJECT_ROOT/src/fetch-usage.mjs" "$mode" "$debug_flag"
100
+ }
101
+
102
+ launch_login() {
103
+ local browser
104
+ browser="$(find_browser)" || die "No encontré Chrome/Chromium. Define CHROME_BIN."
105
+
106
+ if is_login_running; then
107
+ printf 'La sesión ya está activa en background. PID: %s\n' "$(cat "$PID_FILE")"
108
+ printf 'DevTools remoto: http://127.0.0.1:%s\n' "$DEBUG_PORT"
109
+ printf 'Si necesitas reiniciarla, usa `visualizar-r-l stop`.\n'
110
+ return 0
111
+ fi
112
+
113
+ printf 'Abriendo %s en background con perfil persistente en %s\n' "$browser" "$PROFILE_DIR"
114
+ printf 'DevTools remoto: http://127.0.0.1:%s\n' "$DEBUG_PORT"
115
+ printf 'Log: %s\n' "$LOG_FILE"
116
+
117
+ : > "$LOG_FILE"
118
+ setsid "$browser" \
119
+ --user-data-dir="$PROFILE_DIR" \
120
+ --remote-debugging-port="$DEBUG_PORT" \
121
+ --remote-allow-origins='*' \
122
+ --new-window \
123
+ "$URL" \
124
+ >>"$LOG_FILE" 2>&1 < /dev/null &
125
+
126
+ local pid=$!
127
+ echo "$pid" > "$PID_FILE"
128
+ sleep 1
129
+
130
+ if kill -0 "$pid" 2>/dev/null; then
131
+ printf 'Chrome iniciado en background. PID: %s\n' "$pid"
132
+ printf 'Ahora puedes ejecutar `visualizar-r-l` o `visualizar-r-l watch`.\n'
133
+ return 0
134
+ fi
135
+
136
+ rm -f "$PID_FILE"
137
+ die "No pude iniciar Chrome en background. Revisa $LOG_FILE"
138
+ }
139
+
140
+ show_status() {
141
+ if is_login_running; then
142
+ printf 'Sesión activa. PID: %s\n' "$(cat "$PID_FILE")"
143
+ printf 'DevTools remoto: http://127.0.0.1:%s\n' "$DEBUG_PORT"
144
+ printf 'Perfil: %s\n' "$PROFILE_DIR"
145
+ printf 'Log: %s\n' "$LOG_FILE"
146
+ return 0
147
+ fi
148
+
149
+ printf 'No hay una sesión activa en background.\n'
150
+ printf 'Ejecuta `visualizar-r-l login`.\n'
151
+ }
152
+
153
+ stop_login() {
154
+ if ! is_login_running; then
155
+ rm -f "$PID_FILE"
156
+ printf 'No hay una sesión activa en background.\n'
157
+ return 0
158
+ fi
159
+
160
+ local pid
161
+ pid="$(cat "$PID_FILE")"
162
+
163
+ if kill "$pid" 2>/dev/null; then
164
+ rm -f "$PID_FILE"
165
+ printf 'Sesión detenida. PID anterior: %s\n' "$pid"
166
+ return 0
167
+ fi
168
+
169
+ die "No pude detener el proceso $pid"
170
+ }
171
+
172
+ doctor() {
173
+ local browser node_bin npm_bin
174
+ browser="$(find_browser || true)"
175
+ node_bin="$(command -v node || true)"
176
+ npm_bin="$(command -v npm || true)"
177
+
178
+ print_kv "Proyecto" "$PROJECT_ROOT"
179
+ print_kv "State dir" "$STATE_ROOT"
180
+ print_kv "Profile dir" "$PROFILE_DIR"
181
+ print_kv "Browser" "${browser:-missing}"
182
+ print_kv "Node" "${node_bin:-missing}"
183
+ print_kv "npm" "${npm_bin:-missing}"
184
+ print_kv "DevTools" "127.0.0.1:$DEBUG_PORT"
185
+
186
+ if [[ -f "$PROJECT_ROOT/package.json" ]]; then
187
+ print_kv "package.json" "ok"
188
+ else
189
+ print_kv "package.json" "missing"
190
+ fi
191
+
192
+ if [[ -d "$PROJECT_ROOT/node_modules/puppeteer-core" ]]; then
193
+ print_kv "puppeteer" "ok"
194
+ else
195
+ print_kv "puppeteer" "missing"
196
+ fi
197
+
198
+ if is_login_running; then
199
+ print_kv "Session" "active (PID $(cat "$PID_FILE"))"
200
+ else
201
+ print_kv "Session" "inactive"
202
+ fi
203
+ }
204
+
205
+ dump_dom() {
206
+ run_helper raw
207
+ }
208
+
209
+ raw_text() {
210
+ run_helper raw
211
+ }
212
+
213
+ run_once() {
214
+ run_helper render
215
+ }
216
+
217
+ watch_mode() {
218
+ while true; do
219
+ clear
220
+ if ! run_once; then
221
+ exit_code=$?
222
+ printf '\nReintentando en %ss...\n' "$REFRESH_SECONDS" >&2
223
+ sleep "$REFRESH_SECONDS"
224
+ continue
225
+ fi
226
+ sleep "$REFRESH_SECONDS"
227
+ done
228
+ }
229
+
230
+ main() {
231
+ case "${1:-}" in
232
+ "" )
233
+ run_once
234
+ ;;
235
+ watch )
236
+ watch_mode
237
+ ;;
238
+ login )
239
+ launch_login
240
+ ;;
241
+ status )
242
+ show_status
243
+ ;;
244
+ stop )
245
+ stop_login
246
+ ;;
247
+ doctor )
248
+ doctor
249
+ ;;
250
+ raw )
251
+ raw_text
252
+ ;;
253
+ debug )
254
+ run_helper raw --debug
255
+ ;;
256
+ help|-h|--help )
257
+ usage
258
+ ;;
259
+ * )
260
+ usage >&2
261
+ exit 1
262
+ ;;
263
+ esac
264
+ }
265
+
266
+ main "$@"
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "visualizar-rate-limit",
3
+ "version": "0.1.0",
4
+ "description": "TUI para ver el rate limit de Codex desde chatgpt.com/codex/settings/usage",
5
+ "type": "module",
6
+ "bin": {
7
+ "visualizar-r-l": "./bin/visualizar-r-l"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "scripts",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "postinstall": "chmod +x ./bin/visualizar-r-l ./src/fetch-usage.mjs ./scripts/install.sh",
18
+ "doctor": "node ./src/fetch-usage.mjs raw --debug",
19
+ "pack:dry": "npm pack --dry-run"
20
+ },
21
+ "keywords": [
22
+ "codex",
23
+ "chatgpt",
24
+ "tui",
25
+ "rate-limit",
26
+ "cli",
27
+ "puppeteer"
28
+ ],
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "dependencies": {
34
+ "puppeteer-core": "^24.40.0"
35
+ }
36
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ TARGET_DIR="${HOME}/.local/bin"
7
+ TARGET="${TARGET_DIR}/visualizar-r-l"
8
+ SOURCE="${ROOT_DIR}/bin/visualizar-r-l"
9
+
10
+ mkdir -p "$TARGET_DIR"
11
+
12
+ cd "$ROOT_DIR"
13
+ npm install
14
+ chmod +x "$SOURCE" "$ROOT_DIR/src/fetch-usage.mjs"
15
+ ln -sf "$SOURCE" "$TARGET"
16
+
17
+ printf 'Instalado: %s\n' "$TARGET"
18
+ printf 'Prueba: visualizar-r-l doctor\n'
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from 'node:process';
4
+ import puppeteer from 'puppeteer-core';
5
+
6
+ const mode = process.argv[2] || 'render';
7
+ const debug = process.argv.includes('--debug') || process.env.VRL_DEBUG === '1';
8
+ const url = process.env.VRL_URL || 'https://chatgpt.com/codex/settings/usage';
9
+ const browserPath = process.env.CHROME_BIN;
10
+ const userDataDir = process.env.VRL_PROFILE_DIR;
11
+ const debugPort = process.env.VRL_DEBUG_PORT || '9223';
12
+
13
+ if (!browserPath) {
14
+ console.error('No encontré Chrome/Chromium. Define CHROME_BIN o usa el wrapper bash.');
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!userDataDir) {
19
+ console.error('VRL_PROFILE_DIR no está definido.');
20
+ process.exit(1);
21
+ }
22
+
23
+ function logDebug(message) {
24
+ if (debug) console.error(`[debug] ${message}`);
25
+ }
26
+
27
+ function stripHtml(input) {
28
+ return input
29
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
30
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
31
+ .replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, ' ')
32
+ .replace(/<[^>]+>/g, ' ')
33
+ .replace(/&nbsp;/g, ' ')
34
+ .replace(/&amp;/g, '&')
35
+ .replace(/&lt;/g, '<')
36
+ .replace(/&gt;/g, '>')
37
+ .replace(/&#39;/g, "'")
38
+ .replace(/&quot;/g, '"')
39
+ .replace(/\s+/g, ' ')
40
+ .trim();
41
+ }
42
+
43
+ function clamp(value, min, max) {
44
+ return Math.max(min, Math.min(max, value));
45
+ }
46
+
47
+ function extractPercent(segment) {
48
+ const percentMatch = segment.match(/(\d{1,3})\s*%/);
49
+ if (percentMatch) return clamp(Number(percentMatch[1]), 0, 100);
50
+
51
+ const ratioMatch = segment.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/);
52
+ if (ratioMatch) {
53
+ const used = Number(ratioMatch[1]);
54
+ const total = Number(ratioMatch[2]);
55
+ if (total > 0) return clamp(Math.round((used / total) * 100), 0, 100);
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ function extractReset(segment) {
62
+ const resetMatch = segment.match(/(?:reset(?:s|ting)?|renews?|refresh(?:es|ing)?)\s+(?:in|on|at)?\s*([^|]+?)(?=(?:\d{1,3}\s*%|\d+\s*\/\s*\d+|$))/i);
63
+ return resetMatch ? resetMatch[1].trim() : null;
64
+ }
65
+
66
+ function findWindow(text, label, patterns) {
67
+ for (const pattern of patterns) {
68
+ const match = text.match(pattern);
69
+ if (!match) continue;
70
+
71
+ const segment = match[0];
72
+ const percent = extractPercent(segment);
73
+ if (percent === null) continue;
74
+
75
+ return {
76
+ label,
77
+ percent,
78
+ reset: extractReset(segment),
79
+ source: segment.trim(),
80
+ };
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ function fallbackWindows(text) {
87
+ const percents = [...text.matchAll(/(\d{1,3})\s*%/g)]
88
+ .map((m) => clamp(Number(m[1]), 0, 100))
89
+ .slice(0, 2);
90
+
91
+ if (percents.length < 2) return [];
92
+
93
+ return [
94
+ { label: 'Ventana 1', percent: percents[0], reset: null, source: 'fallback' },
95
+ { label: 'Ventana 2', percent: percents[1], reset: null, source: 'fallback' },
96
+ ];
97
+ }
98
+
99
+ function colorize(percent, value) {
100
+ if (!process.stdout.isTTY) return value;
101
+ if (percent >= 90) return `\x1b[31m${value}\x1b[0m`;
102
+ if (percent >= 70) return `\x1b[33m${value}\x1b[0m`;
103
+ return `\x1b[32m${value}\x1b[0m`;
104
+ }
105
+
106
+ function renderBar(percent, width = 38) {
107
+ const filled = Math.round((percent / 100) * width);
108
+ const empty = Math.max(0, width - filled);
109
+ const bar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
110
+ return colorize(percent, bar);
111
+ }
112
+
113
+ function render(text) {
114
+ const windows = [
115
+ findWindow(text, 'Ventana 5h', [
116
+ /(?:5\s*h|5-hour|5 hour|five hour)[\s\S]{0,180}/i,
117
+ /(?:hourly|hours?)[\s\S]{0,180}/i,
118
+ ]),
119
+ findWindow(text, 'Ventana semanal', [
120
+ /(?:weekly|week(?:ly)?|7\s*days?)[\s\S]{0,180}/i,
121
+ ]),
122
+ ].filter(Boolean);
123
+
124
+ const finalWindows = windows.length >= 2 ? windows : fallbackWindows(text);
125
+
126
+ if (finalWindows.length < 2) {
127
+ console.error('No pude detectar las dos ventanas de rate limit en el HTML actual.');
128
+ console.error('Prueba `visualizar-r-l raw` y compárteme el texto para ajustar el parser.');
129
+ process.exit(3);
130
+ }
131
+
132
+ const now = new Date().toLocaleString('es-CL', {
133
+ year: 'numeric',
134
+ month: '2-digit',
135
+ day: '2-digit',
136
+ hour: '2-digit',
137
+ minute: '2-digit',
138
+ second: '2-digit',
139
+ hour12: false,
140
+ });
141
+
142
+ const header = `${'='.repeat(16)} Codex Rate Limit ${'='.repeat(16)}`;
143
+ console.log(header);
144
+ console.log(`Actualizado: ${now}`);
145
+ console.log('');
146
+
147
+ for (const item of finalWindows.slice(0, 2)) {
148
+ const bar = renderBar(item.percent);
149
+ const pct = colorize(item.percent, `${String(item.percent).padStart(3, ' ')}%`);
150
+ console.log(`${item.label.padEnd(16)} ${pct} ${bar}`);
151
+ if (item.reset) console.log(`Reset: ${item.reset}`);
152
+ console.log('');
153
+ }
154
+ }
155
+
156
+ let browser;
157
+ let hardTimeout;
158
+
159
+ try {
160
+ hardTimeout = setTimeout(() => {
161
+ console.error('Timeout duro alcanzado mientras intentaba extraer la página.');
162
+ process.exit(124);
163
+ }, 45000);
164
+
165
+ logDebug(`Conectando a Chrome visible en 127.0.0.1:${debugPort}`);
166
+ browser = await puppeteer.connect({
167
+ browserURL: `http://127.0.0.1:${debugPort}`,
168
+ defaultViewport: null,
169
+ });
170
+ logDebug('Conexión establecida');
171
+
172
+ const existingPages = await browser.pages();
173
+ let page = existingPages.find((candidate) => candidate.url().startsWith('https://chatgpt.com/'));
174
+ if (!page) {
175
+ page = await browser.newPage();
176
+ }
177
+
178
+ page.on('console', (msg) => logDebug(`console:${msg.type()}: ${msg.text()}`));
179
+ page.on('pageerror', (err) => logDebug(`pageerror: ${err.message}`));
180
+ page.on('requestfailed', (req) => logDebug(`requestfailed: ${req.url()} :: ${req.failure()?.errorText || 'unknown'}`));
181
+ page.on('framenavigated', (frame) => {
182
+ if (frame === page.mainFrame()) logDebug(`navegacion: ${frame.url()}`);
183
+ });
184
+ logDebug(`Abriendo ${url}`);
185
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
186
+ logDebug('DOM content loaded');
187
+ await page.waitForFunction(() => document.body && document.body.innerText.length > 50, { timeout: 30000 }).catch(() => {});
188
+ logDebug('Paso waitForFunction');
189
+ await page.waitForNetworkIdle({ idleTime: 1200, timeout: 15000 }).catch(() => {});
190
+ logDebug('Paso waitForNetworkIdle');
191
+ await new Promise((resolve) => setTimeout(resolve, 1500));
192
+ logDebug('Pausa final completada');
193
+
194
+ const payload = await page.evaluate(() => ({
195
+ href: location.href,
196
+ title: document.title,
197
+ text: document.body?.innerText || '',
198
+ html: document.documentElement?.outerHTML || '',
199
+ }));
200
+
201
+ const text = stripHtml(payload.text || payload.html);
202
+ logDebug(`Título detectado: ${payload.title}`);
203
+ logDebug(`URL detectada: ${payload.href}`);
204
+ logDebug(`Largo texto: ${text.length}`);
205
+
206
+ if (!text || !/codex|usage|limit|rate|chatgpt/i.test(text)) {
207
+ console.error('No pude extraer contenido útil desde la página. Ejecuta `visualizar-r-l login` primero.');
208
+ console.error(`URL actual: ${payload.href}`);
209
+ console.error(`Título: ${payload.title}`);
210
+ process.exit(2);
211
+ }
212
+
213
+ if (mode === 'raw') {
214
+ console.log(text);
215
+ } else {
216
+ render(text);
217
+ }
218
+ } catch (error) {
219
+ console.error(error?.stack || String(error));
220
+ process.exit(1);
221
+ } finally {
222
+ if (hardTimeout) clearTimeout(hardTimeout);
223
+ if (browser) {
224
+ await browser.disconnect();
225
+ }
226
+ }