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 +21 -0
- package/README.md +66 -0
- package/bin/visualizar-r-l +266 -0
- package/package.json +36 -0
- package/scripts/install.sh +18 -0
- package/src/fetch-usage.mjs +226 -0
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(/ /g, ' ')
|
|
34
|
+
.replace(/&/g, '&')
|
|
35
|
+
.replace(/</g, '<')
|
|
36
|
+
.replace(/>/g, '>')
|
|
37
|
+
.replace(/'/g, "'")
|
|
38
|
+
.replace(/"/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
|
+
}
|