Recupera los cortes de Kiro, relanza kiro-gateway y añade /kiro.

Español • 🇬🇧 English
Port a Go de kiro-gateway como binario único, sin dependencias de runtime.
Un proxy local que expone las APIs de OpenAI y Anthropic y traduce las peticiones a la API de Kiro (Amazon Q Developer / AWS CodeWhisperer), de forma que cualquier cliente compatible con esas APIs —Claude Code, Cursor, Cline, Roo Code, el SDK de OpenAI, LangChain, Continue, etc.— pueda usar los modelos de Kiro.
Objetivo del port: paridad byte a byte con el original en las fronteras de red (los bytes SSE/JSON que ven los clientes). Los conversores, el tokenizer, el streaming y los formatters se validan contra un corpus golden grabado del upstream fijado (commit
a5292ca, v2.4.dev.13). Ver docs/CORPUS.md y docs/DIFFERENCES.md.
La disponibilidad de modelos depende de tu plan de Kiro (gratuito o de pago): el gateway da acceso a los modelos disponibles en tu IDE o CLI según tu suscripción. Consulta la lista real en tiempo de ejecución con GET /v1/models.
Modelos habituales en el plan gratuito: Claude Sonnet 4.5, Claude Haiku 4.5, Claude Sonnet 4, y varios modelos abiertos (GLM, DeepSeek, MiniMax, Qwen).
💡 Resolución flexible de nombres: puedes usar cualquier formato —
claude-sonnet-4-5,claude-sonnet-4.5o nombres versionados comoclaude-sonnet-4-5-20250929—; el gateway los normaliza automáticamente.
| Característica | Descripción |
|---|---|
| 🔌 API compatible con OpenAI | /v1/chat/completions para cualquier herramienta compatible |
| 🔌 API compatible con Anthropic | Endpoint nativo /v1/messages |
| 🔀 Multi-cuenta con failover | Conmutación automática entre varias cuentas de Kiro |
| 🌐 Soporte VPN/proxy | Proxy HTTP/SOCKS5 para redes restringidas |
| 🧠 Extended thinking | Razonamiento (reasoning) en respuestas |
| 👁️ Visión | Envío de imágenes al modelo |
| 🔍 Búsqueda web | Herramienta web_search vía MCP de Kiro |
| 🛠️ Tool calling | Function calling en ambos dialectos |
| 💬 Historial completo | Se pasa todo el contexto de la conversación |
| 📡 Streaming | SSE completo en ambos dialectos |
| 🔄 Reintentos | Reintentos automáticos ante errores (403, 429, 5xx) |
| 🔐 Gestión de tokens | Refresco automático antes de expirar |
| 🧮 Conteo de tokens fiel | tiktoken cl100k_base embebido; /v1/messages/count_tokens |
Hace falta la sesión iniciada en una de estas fuentes de credenciales de Kiro: Kiro CLI (AWS SSO: Builder ID gratuito o cuenta corporativa) o Kiro IDE.
Con PowerShell 7 (winget install Microsoft.PowerShell) y Claude Code instalados:
irm https://raw.githubusercontent.com/marr-cloud/kiro-gateway-go/main/scripts/get.ps1 | iex
kclaude # en una terminal nueva de pwsh
scripts/get.ps1 baja el zip de Windows de la última release, comprueba su SHA256, lo deja en %LOCALAPPDATA%\Programs\kiro-gateway y ejecuta scripts/install.ps1, que:
.env con una PROXY_API_KEY aleatoria (no se muestra);credentials.json con la cuenta que encuentre (kiro-cli; si no, Kiro IDE);$PROFILE una función kclaude entre dos marcas # >>> kiro-gateway >>>, o la actualiza (si ya tienes un kclaude propio, no lo toca).Nunca pisa un .env ni un credentials.json existentes. Volver a ejecutar la línea actualiza a la última release y conserva tu configuración.
Desde un clon es igual, compilando con Go 1.27+:
git clone https://github.com/marr-cloud/kiro-gateway-go.git
pwsh kiro-gateway-go/scripts/install.ps1 # -Build recompila tras un git pull
kclaude
Después, ver Con Claude Code.
Para otras plataformas o para usar el gateway sin Claude Code. Requiere Go 1.27+ (solo para compilar; el binario resultante no necesita nada instalado).
# Clona el repositorio
git clone https://github.com/marr-cloud/kiro-gateway-go.git
cd kiro-gateway-go
# Compila el binario (usa Taskfile; equivale a `go build ./cmd/kiro-gateway`)
task build
# o directamente:
go build -o kiro-gateway ./cmd/kiro-gateway
# Configura las credenciales: un credentials.json con tus cuentas y un .env con
# PROXY_API_KEY (ver sección Configuración)
# Arranca el servidor
./kiro-gateway
# Con host/puerto personalizados (si el 8000 está ocupado)
./kiro-gateway --port 9000
El servidor queda disponible en http://localhost:8000.
# Prepara credentials.json + un .env con PROXY_API_KEY (ver Configuración)
docker compose up -d
docker compose logs -f
curl http://localhost:8000/health
La imagen es multi-stage sobre distroless/static:nonroot (~27 MB, sin shell, usuario no-root) y su healthcheck usa el propio binario (--health). Ver Dockerfile y docker-compose.yml.
También se publica una imagen multi-arch (linux amd64/arm64) en cada release:
docker pull ghcr.io/marr-cloud/kiro-gateway-go:latest
Binarios de release para las cinco plataformas (Windows/Linux/macOS × amd64/arm64), con
SHA256SUMS, están disponibles en la página de Releases. Descarga el binario de tu plataforma en lugar de compilar, si lo prefieres.
kclaude y /kiro)En Windows, scripts/kiro-claude.ps1 arranca el gateway si no está corriendo y abre Claude Code apuntando a él, con el mod claude-mod/ cargado. La key sale de PROXY_API_KEY en el .env y solo vive en el entorno de ese proceso. Hace falta PowerShell 7 (pwsh): el mod lo usa para lanzar scripts/kiro-gateway.ps1. kclaude es la función que deja scripts/install.ps1 en tu $PROFILE; acepta los mismos argumentos que claude (kclaude -p "hola", kclaude --continue).
Cuando un modelo corta la respuesta, el mod reintenta con el modelo de respaldo que declara Kiro (por ejemplo claude-sonnet-5.5 → claude-sonnet-5) una vez por turno, y el resto del turno sigue con el respaldo. Un corte cuesta 2 peticiones a Kiro si el respaldo responde, o 3 si también corta; sin el mod, Claude Code ya hace 2 por su cuenta. El mod también relanza el gateway si se cayó y añade /kiro:
| Comando | Hace | ||
|---|---|---|---|
/kiro | Versión, uptime, cuenta activa, debug y modelo de la sesión | ||
/kiro restart | Reinicia el gateway conservando su DEBUG_MODE | ||
/kiro logs [n] | Últimas n líneas (20) de gateway.log y gateway.err.log | ||
| `/kiro debug all\ | errors\ | off` | Reinicia con ese DEBUG_MODE y muestra la carpeta de debug |
/kiro models [id] | Tabla de modelos (thinking nativo, effort, respaldo); con id, cómo cambiar a ese modelo solo en esta sesión (/model y s: /model <id> lo guardaría en tus settings globales) |
En auto mode, el modo de permisos por defecto desde Claude Code v2.1.283, Claude Code pide al servidor que revise las acciones. Kiro no hace esa revisión, así que kclaude pone CLAUDE_CODE_AUTO_MODE_SERVER=0 y el clasificador corre desde el cliente, como petición normal al gateway (gasta créditos de Kiro). Si lanzas claude contra el gateway sin kclaude, pon esa variable tú mismo (en la shell o en env de tus settings). Si no, al primer comando que revise el clasificador aparece «this session isn't eligible» y la acción espera a que pulses Enter.
scripts/kiro-gateway.ps1 start|stop|restart|logs hace lo mismo desde la terminal. El gateway que lanza el script no hereda el entorno de la terminal: solo cuentan su .env y el -DebugMode que se le pase.
| Flag | Abreviatura | Descripción |
|---|---|---|
--host | -H | Interfaz de escucha (por defecto SERVER_HOST o 0.0.0.0) |
--port | -p | Puerto de escucha (por defecto SERVER_PORT o 8000) |
--version | -v | Imprime la versión y sale |
--help | -h | Imprime la ayuda y sale |
--health | Consulta GET /health del servidor en marcha y sale con 0 (sano) o 1 |
Dos piezas: credentials.json define las cuentas de Kiro; el .env guarda la clave del proxy y los ajustes de comportamiento. Protege siempre tu proxy con PROXY_API_KEY: es la clave que usarán los clientes al conectarse.
credentials.jsonEl gateway carga las cuentas de un array JSON. Por defecto busca credentials.json en el directorio de trabajo; puedes apuntar a otra ruta con ACCOUNTS_CONFIG_FILE. Cada entrada lleva un type (json, sqlite o refresh_token), enabled: true, y la ruta o el token correspondiente. Ver credentials.json.example.
[
{ "type": "json", "enabled": true, "path": "C:/Users/tu-usuario/.aws/sso/cache/kiro-auth-token.json" },
{ "type": "sqlite", "enabled": true, "path": "C:/Users/tu-usuario/AppData/Local/kiro-cli/data.sqlite3" }
]
json — token de Kiro IDE / Enterprise (válido si contiene refreshToken o clientId).sqlite — base de datos de kiro-cli (válida si tiene la tabla auth_kv).refresh_token — un token de refresco directo ("refresh_token": "eyJ...", opcional profile_arn).Con varias entradas el gateway hace failover automático: cuando una devuelve un error (429, 402) pasa a la siguiente; si una falla varias veces seguidas la aparta y la reintenta periódicamente. Con una sola cuenta no hay conmutación (se devuelve el error real de Kiro).
El port no implementa el modo de "cuenta única" del original (
REFRESH_TOKEN/KIRO_CREDS_FILE/KIRO_CLI_DB_FILEsueltos en el.envcomo fuente de cuenta): las cuentas se definen siempre encredentials.json. Ver docs/DIFFERENCES.md.
models.json (override opcional)Por defecto, GET /v1/models descubre los modelos dinámicamente consultando tu cuenta de Kiro (igual que hace el propio Kiro CLI), así que la lista refleja lo que hoy tienes disponible sin tocar nada. Si el descubrimiento falla, cae a una lista estática incorporada.
Para fijar o filtrar la lista, crea un models.json (array JSON de IDs); si existe, es la lista autoritativa y anula el descubrimiento. Apunta a otra ruta con MODELS_CONFIG_FILE. Ver models.json.example.
["auto", "claude-opus-5", "claude-sonnet-5", "claude-opus-4.8", "claude-haiku-4.5"]
No hace falta listar un modelo para usarlo. El gateway es passthrough ("gateway, not gatekeeper"): puedes pedir cualquier
modelen tus peticiones y se pasa tal cual a Kiro, esté o no en/v1/models.models.jsonsolo controla lo que aparece en el listado.
.envCopia .env.example a .env y ajústalo. Lo mínimo es PROXY_API_KEY:
# Contraseña para proteger TU proxy (inventa una cadena segura)
PROXY_API_KEY="mi-contraseña-super-secreta-123"
# Opcional: ruta al fichero de cuentas (por defecto: credentials.json en el cwd)
ACCOUNTS_CONFIG_FILE=C:/Users/tu-usuario/kiro/kiro-gateway/credentials.json
Para redes restringidas o problemas de conectividad con AWS:
VPN_PROXY_URL=http://127.0.0.1:7890 # HTTP
# VPN_PROXY_URL=socks5://127.0.0.1:1080 # SOCKS5
| Variable | Por defecto | Descripción |
|---|---|---|
ACCOUNTS_CONFIG_FILE | credentials.json | Ruta al array de cuentas |
MODELS_CONFIG_FILE | models.json | Lista opcional de modelos para /v1/models (si el fichero existe) |
SERVER_HOST / SERVER_PORT | 0.0.0.0 / 8000 | Interfaz y puerto de escucha |
KIRO_REGION / KIRO_API_REGION | us-east-1 | Región de OIDC / de la API de Kiro |
WEB_SEARCH_ENABLED | true | Habilita la herramienta de búsqueda web |
TRUNCATION_RECOVERY | true | Recuperación ante respuestas truncadas |
DEBUG_MODE | off | Modo de log de depuración a ficheros (off / errors / all) |
LOG_LEVEL | INFO | Verbosidad del log de peticiones en stdout (DEBUG / INFO / WARN / ERROR / OFF) |
| Endpoint | Método | Descripción |
|---|---|---|
/ | GET | Estado básico (JSON) |
/health | GET | Estado detallado (JSON) |
/v1/models | GET | Lista de modelos disponibles |
/v1/chat/completions | POST | API Chat Completions de OpenAI |
/v1/messages | POST | API Messages de Anthropic |
/v1/messages/count_tokens | POST | Conteo de tokens (Anthropic) |
/kiro/status | GET | Estado para el mod de Claude Code: versión, cuenta, debug y modelos con su respaldo por refusal |
Autenticación: Authorization: Bearer <PROXY_API_KEY> (dialecto OpenAI) o x-api-key: <PROXY_API_KEY> (dialecto Anthropic).
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer mi-contraseña-super-secreta-123" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"messages": [{"role": "user", "content": "¡Hola!"}],
"stream": true
}'
curl http://localhost:8000/v1/messages \
-H "x-api-key: mi-contraseña-super-secreta-123" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "¡Hola!"}]
}'
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="mi-contraseña-super-secreta-123", # tu PROXY_API_KEY
)
response = client.chat.completions.create(
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": "¡Hola!"}],
stream=True,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
El log de depuración está desactivado por defecto. Para activarlo:
# off: desactivado (por defecto)
# errors: guarda logs solo de peticiones fallidas (4xx, 5xx) — recomendado
# all: guarda logs de todas las peticiones (se sobrescriben en cada una)
DEBUG_MODE=errors
Los ficheros se escriben en debug_logs/ (request_body.json, kiro_request_body.json, response_stream_raw.txt, response_stream_modified.txt, etc.).
Este port replica el comportamiento del upstream en las fronteras de red, con desviaciones deliberadas y acotadas (p. ej. no expone /docs, /redoc ni /openapi.json de FastAPI; los endpoints / y /health devuelven JSON con campos extra; añade el flag --health). La lista completa y el porqué de cada una está en docs/DIFFERENCES.md.
AGPL-3.0. Este proyecto es obra derivada de jwadow/kiro-gateway (v2.4.dev.13, commit a5292ca), distribuido bajo la misma licencia. Ver LICENSE y NOTICE para la atribución completa y la lista de cambios. Si este proyecto te resulta útil, considera apoyar el proyecto original.
Este proyecto no está afiliado ni respaldado por AWS, Anthropic ni Kiro IDE. Úsalo bajo tu propia responsabilidad y respetando los términos de servicio de las APIs subyacentes.
hooks/register.ts 160 lines1// Mod `kiro`: hace que Claude Code sobre kiro-gateway no se corte.
2// - Un refusal reintenta con el modelo de respaldo que declara Kiro, una vez por turno;
3// el resto de ese turno sigue con el respaldo.
4// - Un gateway caído se relanza con scripts/kiro-gateway.ps1.
5// - /kiro controla el gateway (estado, restart, logs, debug, models).
6// Fuera de un gateway local (ANTHROPIC_BASE_URL) queda inerte.
7import type { EngineInterface, Register } from 'claude-code'
8import { USAGE, fallbackMap, formatModels, formatStatus, localBase, modelKey, parseArgs, type Status } from './kiro.ts'
9
10// Estado del módulo: un reload vuelve a lanzar session.start y lo rehace.
11// Los helpers viven a nivel de módulo: el cargador de hooks exige que toda función que reciba $ se declare ahí.
12const state = { base: '', port: '', token: '', fallbacks: new Map<string, string>(), rescued: new Map<string, string>() }
13
14const scriptPath = ($: EngineInterface) =>
15 `${$.plugin.root.replace(/[\\/]\.claude-plugin[\\/]?$/, '')}/../scripts/kiro-gateway.ps1`
16
17async function healthy($: EngineInterface): Promise<boolean> {
18 try {
19 return (await $.http.fetch(`${state.base}/health`)).ok
20 } catch {
21 return false
22 }
23}
24
25async function fetchStatus($: EngineInterface): Promise<Status> {
26 const r = await $.http.fetch(`${state.base}/kiro/status`, { headers: { Authorization: `Bearer ${state.token}` } })
27 if (r.status === 401) throw new Error('401: la key de kclaude no coincide con PROXY_API_KEY del .env del gateway.')
28 if (!r.ok) throw new Error(`/kiro/status respondió ${r.status}`)
29 const st = JSON.parse(r.text) as Status
30 state.fallbacks = fallbackMap(st.models)
31 return st
32}
33
34async function runScript($: EngineInterface, args: string[]): Promise<{ ok: boolean; text: string }> {
35 const r = await $.process.run(
36 ['pwsh', '-NoProfile', '-NonInteractive', '-File', scriptPath($), ...args, '-Port', state.port],
37 { timeoutMs: 60_000 },
38 )
39 return { ok: r.exitCode === 0, text: `${r.stdout}${r.stderr}`.trim() }
40}
41
42const DEBUG_MODES = ['all', 'errors', 'off']
43
44export const register: Register = on => {
45 on('session.start', async ($, e, next) => {
46 const started = await next(e)
47 // Reinicio: el estado es del módulo y sobrevive entre sesiones; fuera de un gateway local debe quedar vacío.
48 state.base = ''
49 state.port = ''
50 state.token = ''
51 state.fallbacks = new Map()
52 state.rescued = new Map()
53 const local = localBase(await $.env.get('ANTHROPIC_BASE_URL'))
54 if (!local) return started
55 state.base = local.base
56 state.port = local.port
57 state.token = (await $.env.get('ANTHROPIC_AUTH_TOKEN')) ?? ''
58 await $.command.register({
59 name: 'kiro',
60 description: 'Estado y control de kiro-gateway',
61 argumentHint: '[restart | logs [n] | debug all|errors|off | models [id]]',
62 })
63 await fetchStatus($).catch(() => undefined) // sin estado: /kiro lo dirá
64 return started
65 })
66
67 on('turn.step', async function* ($, e, next) {
68 if (!state.base) return yield* next(e)
69
70 if (!(await healthy($))) {
71 const r = await runScript($, ['start'])
72 $.ui.toast(r.ok ? 'kiro-gateway relanzado' : `kiro-gateway no arrancó: ${r.text}`, { timeoutMs: 8000 })
73 if (r.ok) await fetchStatus($).catch(() => undefined)
74 }
75
76 // Turno ya rescatado: el original volvería a cortar, así que el resto del turno va directo
77 // al respaldo, sin toast. Incluye el reintento propio de CC (mismo turnId, otro index).
78 // Si el respaldo también corta, el mod no reintenta más.
79 const sticky = state.rescued.get(e.turnId)
80 if (sticky) return yield* next({ ...e, model: sticky })
81
82 // El motor real entrega el refusal en el chunk 'stop' y deja stopReason en null
83 // en el resultado de next(); se mira en ambos sitios.
84 const gen = next(e)
85 let refused = false
86 let first
87 let done = false
88 try {
89 for (;;) {
90 const r = await gen.next()
91 if (r.done) {
92 done = true
93 first = r.value
94 break
95 }
96 if (r.value.kind === 'stop' && r.value.stopReason === 'refusal') refused = true
97 yield r.value
98 }
99 } finally {
100 // yield* delegaba return()/throw(); el bucle manual debe cerrar el stream de abajo.
101 if (!done) await gen.return(undefined as never)
102 }
103 const fallback = state.fallbacks.get(modelKey(e.model))
104 if (!(refused || first.stopReason === 'refusal') || !fallback || next.signal.aborted) return first
105 // Un Map por turnId y no un único hueco: los subagentes en paralelo intercalan turnos.
106 state.rescued.set(e.turnId, fallback)
107 $.ui.toast(`${e.model} cortó → reintento con ${fallback}`, { timeoutMs: 8000 })
108 return yield* next({ ...e, model: fallback })
109 })
110
111 on('command.run', { command: 'kiro' }, async ($, e) => {
112 if (!state.base) return { text: 'El mod kiro solo actúa con ANTHROPIC_BASE_URL apuntando a un gateway local (kclaude).' }
113 const { sub, rest } = parseArgs(e.args)
114 try {
115 switch (sub) {
116 case '':
117 return { text: formatStatus(await fetchStatus($), await $.session.model()) }
118 case 'restart': {
119 const st = await fetchStatus($).catch(() => undefined)
120 const r = await runScript($, ['restart', ...(st ? ['-DebugMode', st.debug.mode] : [])])
121 if (r.ok) await fetchStatus($).catch(() => undefined)
122 return { text: r.text }
123 }
124 case 'logs': {
125 const n = rest[0] === undefined ? 20 : Number(rest[0])
126 if (!Number.isInteger(n) || n <= 0) return { text: 'Uso: /kiro logs [n]' }
127 return { text: (await runScript($, ['logs', '-Lines', String(n)])).text }
128 }
129 case 'debug': {
130 const mode = rest[0] ?? ''
131 if (!DEBUG_MODES.includes(mode)) return { text: 'Uso: /kiro debug all|errors|off' }
132 const r = await runScript($, ['restart', '-DebugMode', mode])
133 if (!r.ok) return { text: r.text }
134 const st = await fetchStatus($)
135 return { text: `${r.text}\ndebug: ${st.debug.mode} → ${st.debug.dir}` }
136 }
137 case 'models': {
138 const st = await fetchStatus($)
139 const id = rest[0]
140 if (!id) return { text: formatModels(st.models) }
141 if (!st.models.some(m => modelKey(m.id) === modelKey(id))) {
142 return { text: `${id} no está en la lista de Kiro. /kiro models para verla.` }
143 }
144 // No se lanza /model: `/model <id>` escrito guarda el modelo en los settings globales
145 // (~/.claude/settings.json) y el `claude` normal arrancaría con un id de Kiro. El selector
146 // de /model con `s` lo cambia solo para esta sesión, y un mod no tiene otra forma de hacerlo.
147 return {
148 text: `Para usar ${id} solo en esta sesión: /model, elige ${id} y pulsa s.\n(/model ${id} escrito así lo guarda como predeterminado de todas tus sesiones de Claude Code.)`,
149 }
150 }
151 default:
152 return { text: USAGE }
153 }
154 } catch (err) {
155 if (!(await healthy($))) return { text: `kiro-gateway no responde en ${state.base}. Prueba /kiro restart.` }
156 return { text: err instanceof Error ? err.message : String(err) }
157 }
158 })
159}
160hooks/kiro.ts 76 lines1// Funciones puras del mod: sin `$`, para que los tests las prueben sueltas.
2
3/** Un modelo tal como lo devuelve GET /kiro/status. */
4export type ModelInfo = {
5 id: string
6 native_thinking: string[]
7 effort_levels: string[]
8 refusal_fallback: string
9}
10
11/** El cuerpo de GET /kiro/status (DIFFERENCES §20 del gateway). */
12export type Status = {
13 version: string
14 uptime_seconds: number
15 active_account: string | null
16 debug: { mode: string; dir: string }
17 models: ModelInfo[]
18}
19
20export const USAGE = 'Uso: /kiro [restart | logs [n] | debug all|errors|off | models [id]]'
21
22/**
23 * Clave común para los ids de Claude Code (claude-sonnet-5-5, con o sin
24 * sufijo [1m]) y los de Kiro (claude-sonnet-5.5).
25 */
26export function modelKey(id: string): string {
27 return id.trim().toLowerCase().replace(/\[[^\]]*\]$/, '').replace(/\./g, '-')
28}
29
30/** modelKey(id) → modelo de respaldo, solo para los modelos que lo declaran. */
31export function fallbackMap(models: readonly ModelInfo[]): Map<string, string> {
32 const map = new Map<string, string>()
33 for (const m of models) if (m.refusal_fallback) map.set(modelKey(m.id), m.refusal_fallback)
34 return map
35}
36
37/** La base y el puerto si url es un gateway local; undefined en otro caso. */
38export function localBase(url: string | undefined): { base: string; port: string } | undefined {
39 const m = /^http:\/\/(127\.0\.0\.1|localhost):(\d+)\/?$/.exec(url ?? '')
40 if (!m) return undefined
41 return { base: `http://${m[1]}:${m[2]}`, port: m[2]! }
42}
43
44export function parseArgs(args: string): { sub: string; rest: string[] } {
45 const parts = args.trim().split(/\s+/).filter(Boolean)
46 return { sub: parts[0] ?? '', rest: parts.slice(1) }
47}
48
49function formatUptime(seconds: number): string {
50 const h = Math.floor(seconds / 3600)
51 const m = Math.floor((seconds % 3600) / 60)
52 return h > 0 ? `${h} h ${m} min` : `${m} min`
53}
54
55export function formatStatus(st: Status, sessionModel: string): string {
56 return [
57 `kiro-gateway ${st.version} · activo hace ${formatUptime(st.uptime_seconds)}`,
58 `cuenta: ${st.active_account ?? '(ninguna)'}`,
59 `debug: ${st.debug.mode} (${st.debug.dir})`,
60 `modelo de la sesión: ${sessionModel}`,
61 ].join('\n')
62}
63
64export function formatModels(models: readonly ModelInfo[]): string {
65 const head = ['modelo', 'thinking nativo', 'effort', 'respaldo']
66 const rows = models.map(m => [
67 m.id,
68 m.native_thinking.join(',') || '-',
69 m.effort_levels.join(',') || '-',
70 m.refusal_fallback || '-',
71 ])
72 const all = [head, ...rows]
73 const widths = head.map((_, i) => Math.max(...all.map(r => r[i]!.length)))
74 return all.map(r => r.map((c, i) => c.padEnd(widths[i]!)).join(' ').trimEnd()).join('\n')
75}
76