nexakit 0.1.0 → 0.2.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/.claude-plugin/marketplace.json +18 -1
- package/README.md +40 -30
- package/assets/brand/README.md +0 -2
- package/assets/runtime/package-lock.json +2 -2
- package/bin/agent-task.js +12 -10
- package/bin/agent-toolkit-gemini.js +11 -9
- package/bin/agent-toolkit-inbox.js +12 -10
- package/bin/agent-toolkit-mcp.js +14 -8
- package/bin/agent-toolkit.js +12 -9
- package/bin/nexakit.js +12 -9
- package/bin/project-memory-mcp.js +12 -9
- package/bin/project-memory.js +5 -3
- package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills.json +70 -35
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
- package/package.json +5 -3
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/schemas/skill-catalog.schema.json +9 -2
- package/skills/cerrar-memoria/SKILL.md +6 -2
- package/skills/cerrar-memoria/references/cierre.md +5 -1
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/gemini/SKILL.md +1 -1
- package/skills/project-handoff/SKILL.md +39 -12
- package/skills/project-handoff/references/handoff-template.md +24 -17
- package/skills/quality-gate/SKILL.md +6 -12
- package/skills/revisar/SKILL.md +6 -16
- package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +53 -475
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
- package/skills/web-design-guidelines/SKILL.md +20 -30
- package/src/agent-models.js +53 -13
- package/src/brand.js +117 -0
- package/src/cli-executable.js +5 -3
- package/src/cli.js +16 -9
- package/src/connect.js +56 -4
- package/src/init.js +9 -1
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/link-states.js +172 -0
- package/src/memory-locator.js +118 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/panel-cli.js +182 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +26 -27
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +321 -208
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +8 -5
- package/src/sync-files.js +2 -1
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +397 -70
- package/src/toolkit-commands-guide.js +53 -39
- package/src/toolkit-setup.js +507 -110
- package/assets/brand/agent-toolkit-mark-v1.png +0 -0
- package/skills/depurar/SKILL.md +0 -69
- package/skills/depurar/agents/openai.yaml +0 -7
- package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
- package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
- package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
- package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
- package/skills/estado/SKILL.md +0 -41
- package/skills/programar/SKILL.md +0 -19
- package/skills/spec-driven-development/SKILL.md +0 -48
- package/skills/spec-driven-development/agents/openai.yaml +0 -7
- package/skills/spec-driven-development/references/spec-template.md +0 -55
- package/skills/spec-driven-development/references/workflow.md +0 -70
- package/src/toolkit-tui.js +0 -961
|
@@ -20,7 +20,7 @@ El usuario aprobó en el chat la planificación en lectura y preparar el worktre
|
|
|
20
20
|
run_codex_plan devolvió «La ejecución fue cancelada; no se llamó al modelo».
|
|
21
21
|
No se reintentó ni se avanzaron manualmente las etapas del almacén de tareas.
|
|
22
22
|
|
|
23
|
-
El
|
|
23
|
+
El plan preparado por la sesión actual (en la memoria de Obsidian de NexaKit (`03-IMPLEMENTACION/Planes/2026-10-28-recomendador-skills-plan.md`)
|
|
24
24
|
permite revisar el encargo. Ejecutar el runner nativo requiere una vista previa
|
|
25
25
|
vigente y completar la confirmación interactiva del MCP.
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Asistente por pasos de skills verificadas por SHA-256, coordinación entre agentes y memoria por proyecto para Claude Code y Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
"agent-toolkit-gemini": "./bin/agent-toolkit-gemini.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node --test",
|
|
17
|
+
"test": "node --test --import ./test-support/git-quiet.js \"test/*.test.js\"",
|
|
18
18
|
"catalog:validate": "node scripts/validate-skill-catalog.js",
|
|
19
19
|
"catalog:files": "node scripts/validate-skill-catalog.js --files",
|
|
20
|
-
"
|
|
20
|
+
"catalog:add": "node scripts/add-catalog-skill.js",
|
|
21
|
+
"prepack": "node scripts/prepare-package.js",
|
|
22
|
+
"test:mod": "claude plugin test plugins/nexakit-modelo"
|
|
21
23
|
},
|
|
22
24
|
"engines": {
|
|
23
25
|
"node": ">=24"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"nexakit-modelo","version":"0.1.0","description":"Muestra modelo y esfuerzo observados frente a una recomendación, sin cambiarlos."}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"modules":["./register.js"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { modelStatus } from './status.js';
|
|
2
|
+
async function redraw($, observed, recommendation) {
|
|
3
|
+
const model = await $.session.model();
|
|
4
|
+
$.ui.status(modelStatus(model, observed?.model === model ? observed.effort : null, recommendation));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function register(on) {
|
|
8
|
+
let recommendation = null;
|
|
9
|
+
let observed = null;
|
|
10
|
+
on('session.start', async ($, e, next) => {
|
|
11
|
+
await $.tool.register({
|
|
12
|
+
name: 'nexakit_recomendacion',
|
|
13
|
+
description: 'Registra una recomendación de modelo y esfuerzo para mostrarla, sin cambiar la sesión.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: { model: { type: 'string' }, effort: { type: 'string' }, reason: { type: 'string' } },
|
|
17
|
+
required: ['model', 'effort', 'reason'],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
await redraw($, observed, recommendation);
|
|
22
|
+
return next(e);
|
|
23
|
+
});
|
|
24
|
+
on('tool.call', { tool: 'mcp__nexakit-modelo__nexakit_recomendacion' }, async ($, e) => {
|
|
25
|
+
recommendation = { model: e.model, effort: e.effort, reason: e.reason };
|
|
26
|
+
await redraw($, observed, recommendation);
|
|
27
|
+
return { result: 'Recomendación mostrada; no se cambió el modelo ni el esfuerzo.' };
|
|
28
|
+
});
|
|
29
|
+
on('turn.step', async function* ($, e, next) {
|
|
30
|
+
if (!e.agentId) {
|
|
31
|
+
observed = { model: e.model, effort: e.effort ?? null };
|
|
32
|
+
await redraw($, observed, recommendation);
|
|
33
|
+
}
|
|
34
|
+
yield* next(e);
|
|
35
|
+
});
|
|
36
|
+
on('turn.complete', async ($, e, next) => {
|
|
37
|
+
// Effort can change between turns; never present the previous request as current.
|
|
38
|
+
observed = null;
|
|
39
|
+
await redraw($, observed, recommendation);
|
|
40
|
+
return next(e);
|
|
41
|
+
});
|
|
42
|
+
on('session.end', async ($, e, next) => {
|
|
43
|
+
recommendation = null;
|
|
44
|
+
observed = null;
|
|
45
|
+
$.ui.status(undefined);
|
|
46
|
+
return next(e);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const family = (model) => model?.match(/(?:^|claude-)(haiku|sonnet|opus|fable)(?:-|$)/u)?.[1] ?? model;
|
|
2
|
+
export function modelStatus(model, effort, recommendation) {
|
|
3
|
+
const actual = `${model ?? 'modelo no informado'} · ${effort ?? 'esfuerzo no informado'}`;
|
|
4
|
+
if (!recommendation) return actual;
|
|
5
|
+
const matches =
|
|
6
|
+
effort && family(model) === family(recommendation.model) && effort === recommendation.effort;
|
|
7
|
+
return `${actual} ${matches ? '✓' : `→ recomendado: ${recommendation.model} · ${recommendation.effort}`}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from 'claude-code/testing';
|
|
2
|
+
|
|
3
|
+
describe('nexakit-modelo API', () => {
|
|
4
|
+
test('registers the recommendation tool and reads the session model without a model call', async ($, on) => {
|
|
5
|
+
const tools: string[] = [];
|
|
6
|
+
const statuses: string[] = [];
|
|
7
|
+
on('session.start', ($, e) => ({ cwd: e.cwd }));
|
|
8
|
+
on('session.model', () => ({ value: 'haiku' }));
|
|
9
|
+
on('tool.register', ($, e) => {
|
|
10
|
+
tools.push(e.name);
|
|
11
|
+
return { value: { tool: e.name } };
|
|
12
|
+
});
|
|
13
|
+
on('ui.status', ($, e) => {
|
|
14
|
+
statuses.push(e.text ?? '');
|
|
15
|
+
return { value: undefined };
|
|
16
|
+
});
|
|
17
|
+
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/test' });
|
|
18
|
+
expect(tools).toContain('nexakit_recomendacion');
|
|
19
|
+
expect(statuses.some((text) => text.includes('esfuerzo no informado'))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "nexakit-panel", "version": "0.1.0", "description": "Panel en vivo de la orquestación: modelo líder, subagentes, consultas al asesor, registro de la sesión, contexto y límites.", "types": "./types/index.d.ts", "license": "MIT"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# nexakit-panel
|
|
2
|
+
|
|
3
|
+
Panel de la orquestación de una sesión de Claude Code: el modelo líder y su esfuerzo, las consultas al asesor,
|
|
4
|
+
los subagentes con su modelo, estado y parte del uso, un registro de la sesión, el contexto, los límites de
|
|
5
|
+
5 horas y semanal, y el modelo frente al recomendado por `nexakit-modelo`. Todo sale de lo que informa Claude
|
|
6
|
+
Code; no muestra datos inventados.
|
|
7
|
+
|
|
8
|
+
## Uso
|
|
9
|
+
|
|
10
|
+
- `/panel` abre el panel junto al chat. Con al menos 104 columnas dibuja el diagrama completo; si no, una lista.
|
|
11
|
+
- `/panel-demo` alterna datos de ejemplo, marcados como ficticios, para ver el diseño.
|
|
12
|
+
- Para verlo siempre completo, abre `nexakit panel` en otra división de la terminal: lee la foto que este mod
|
|
13
|
+
guarda en `~/.nexakit/panel/` y la dibuja a pantalla completa.
|
|
14
|
+
|
|
15
|
+
La foto se guarda en tu disco con los secretos evidentes tachados (variables `*TOKEN*`, `*KEY*`, `*SECRET*`,
|
|
16
|
+
`*PASSWORD*`, cabeceras `Bearer` y opciones `--password`).
|
|
17
|
+
|
|
18
|
+
## Pruebas
|
|
19
|
+
|
|
20
|
+
`claude plugin test plugins/nexakit-panel`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"modules":["./register.tsx"]}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Dibuja la orquesta carácter a carácter, en posiciones fijas, como una pantalla de terminal de 104 columnas.
|
|
2
|
+
// Devuelve filas de tramos con estilo; la terminal (nexakit panel) y el panel de Claude los pintan igual.
|
|
3
|
+
import {
|
|
4
|
+
alias, barra, barraEsfuerzo, duracion, estadoPorFamilia, etiquetaEnjambre, etiquetaModelo, familia, hora,
|
|
5
|
+
resumenDelegacion, tituloDeOrquesta, tokens,
|
|
6
|
+
} from './panel.js';
|
|
7
|
+
|
|
8
|
+
export const ANCHO = 104;
|
|
9
|
+
export const COLOR = {
|
|
10
|
+
sonnet: '#fb923c', haiku: '#60a5fa', opus: '#a78bfa', fable: '#f472b6', otro: '#9ca3af',
|
|
11
|
+
ok: '#4ade80', error: '#f87171', texto: '#e5e7eb', tenue: '#6b7280', resalte: '#2e2a4a',
|
|
12
|
+
codex: '#2dd4bf', antigravity: '#fbbf24', claude: '#fb923c',
|
|
13
|
+
};
|
|
14
|
+
const NOMBRE_IA = { codex: 'Codex', antigravity: 'Antigravity', claude: 'Claude' };
|
|
15
|
+
const colorIA = (ia) => COLOR[ia] ?? COLOR.otro;
|
|
16
|
+
const REVISION = { x: 84, w: 20, h: 8 };
|
|
17
|
+
const colorDe = (modelo) => COLOR[familia(modelo)] ?? COLOR.otro;
|
|
18
|
+
|
|
19
|
+
// Geometría, tomada de la imagen de referencia.
|
|
20
|
+
const CX = 66; // eje del flujo central
|
|
21
|
+
const ASESOR = { x: 2, w: 26 };
|
|
22
|
+
const LIDER = { w: 34, y: 4, h: 7 };
|
|
23
|
+
const DELEGACION = { w: 52, y: 12, h: 7 };
|
|
24
|
+
const SUB = { xs: [29, 54, 79], w: 24, y: 23, h: 8 };
|
|
25
|
+
const VUELTA = { w: 40, y: 34, h: 4 };
|
|
26
|
+
const REGISTRO_Y = 39;
|
|
27
|
+
|
|
28
|
+
function lienzo(alto) {
|
|
29
|
+
const celdas = Array.from({ length: alto }, () => Array.from({ length: ANCHO }, () => ({ ch: ' ' })));
|
|
30
|
+
const poner = (x, y, txt, estilo = {}) => {
|
|
31
|
+
[...String(txt)].forEach((ch, i) => {
|
|
32
|
+
const xx = x + i;
|
|
33
|
+
if (y >= 0 && y < alto && xx >= 0 && xx < ANCHO) celdas[y][xx] = { ch, ...estilo };
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const corto = (txt, ancho) => ([...String(txt)].length <= ancho ? String(txt) : `${[...String(txt)].slice(0, Math.max(0, ancho - 1)).join('')}…`);
|
|
37
|
+
const centro = (x, w, y, txt, estilo) => {
|
|
38
|
+
const t = corto(txt, w);
|
|
39
|
+
poner(x + Math.floor((w - [...t].length) / 2), y, t, estilo);
|
|
40
|
+
};
|
|
41
|
+
const caja = (x, y, w, h, color, titulo) => {
|
|
42
|
+
const borde = { c: color };
|
|
43
|
+
poner(x, y, `┌${'─'.repeat(w - 2)}┐`, borde);
|
|
44
|
+
for (let i = 1; i < h - 1; i++) { poner(x, y + i, '│', borde); poner(x + w - 1, y + i, '│', borde); }
|
|
45
|
+
poner(x, y + h - 1, `└${'─'.repeat(w - 2)}┘`, borde);
|
|
46
|
+
if (titulo) poner(x + 4, y, `┤ ${titulo} ├`, { c: COLOR.tenue });
|
|
47
|
+
};
|
|
48
|
+
return { celdas, poner, centro, caja, corto };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Junta las celdas contiguas con el mismo estilo en tramos.
|
|
52
|
+
function aTramos(celdas) {
|
|
53
|
+
return celdas.map((fila) => {
|
|
54
|
+
const tramos = [];
|
|
55
|
+
for (const { ch, ...estilo } of fila) {
|
|
56
|
+
const ultimo = tramos.at(-1);
|
|
57
|
+
const clave = JSON.stringify(estilo);
|
|
58
|
+
if (ultimo && ultimo.clave === clave) ultimo.t += ch;
|
|
59
|
+
else tramos.push({ t: ch, ...estilo, clave });
|
|
60
|
+
}
|
|
61
|
+
return tramos.map(({ clave, ...resto }) => resto);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const texto = (filas) => filas.map((tramos) => tramos.map((x) => x.t).join(''));
|
|
66
|
+
|
|
67
|
+
export function dibujar(estado, { ahora, uso, carpeta = '', filasMax, asesorModelo = null, esEjemplo = false, codex = null } = {}) {
|
|
68
|
+
const s = estado;
|
|
69
|
+
const vacio = s.pasos === 0 && s.agentes.length === 0 && s.registro.length === 0;
|
|
70
|
+
const medidores = medidoresDeUso(uso);
|
|
71
|
+
|
|
72
|
+
if (vacio) {
|
|
73
|
+
const L = lienzo(codex ? 8 : 7);
|
|
74
|
+
L.centro(0, ANCHO, 0, 'NEXAKIT · ORQUESTA', { c: COLOR.opus, b: true });
|
|
75
|
+
L.poner(2, 1, '═'.repeat(ANCHO - 4), { c: COLOR.tenue });
|
|
76
|
+
L.centro(0, ANCHO, 3, 'Aún no hay actividad en esta sesión.', { c: COLOR.texto });
|
|
77
|
+
L.centro(0, ANCHO, 4, 'Trabaja con normalidad: este panel se llena solo.', { c: COLOR.tenue });
|
|
78
|
+
if (medidores.length) pintarMedidores(L, 6, medidores);
|
|
79
|
+
if (codex) pintarCodex(L, medidores.length ? 7 : 6, codex, ahora);
|
|
80
|
+
return { filas: aTramos(L.celdas), ancho: ANCHO, alto: codex ? 8 : 7 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// El registro ocupa lo que sobre: entre 1 y 8 líneas (4 por defecto, como la referencia).
|
|
84
|
+
// Filas fijas: hasta el registro, sus dos bordes, una en blanco, la orden, el estado y el uso si lo hay.
|
|
85
|
+
const lineaModelo = s.lider.modelo ? modeloFrenteARecomendado(s) : null;
|
|
86
|
+
const fijas = REGISTRO_Y + 2 + 3 + (medidores.length ? 1 : 0) + (lineaModelo ? 1 : 0) + (codex ? 1 : 0);
|
|
87
|
+
const lineasRegistro = Math.max(1, Math.min(8, filasMax ? filasMax - fijas : 4, s.registro.length || 1));
|
|
88
|
+
const alto = fijas + lineasRegistro;
|
|
89
|
+
const L = lienzo(alto);
|
|
90
|
+
const r = resumenDelegacion(s.agentes);
|
|
91
|
+
const lider = etiquetaModelo(s.lider.modelo);
|
|
92
|
+
const cLider = colorDe(s.lider.modelo);
|
|
93
|
+
const enjambre = etiquetaEnjambre(s.agentes);
|
|
94
|
+
const cEnjambre = s.agentes.length ? colorDe([...s.agentes].reverse().find((a) => etiquetaModelo(a.modelo) === enjambre)?.modelo) : COLOR.haiku;
|
|
95
|
+
|
|
96
|
+
// ---- Cabecera
|
|
97
|
+
const titulo = tituloDeOrquesta(s);
|
|
98
|
+
const x0 = Math.floor((ANCHO - [...titulo].length) / 2);
|
|
99
|
+
let x = x0;
|
|
100
|
+
for (const [i, parte] of titulo.split(' · ').entries()) {
|
|
101
|
+
const sep = i === 0 ? '' : ' · ';
|
|
102
|
+
L.poner(x, 0, sep, { c: COLOR.tenue }); x += sep.length;
|
|
103
|
+
const color = /DIRIGE/.test(parte) ? cLider : /ENJAMBRE/.test(parte) ? cEnjambre : COLOR.texto;
|
|
104
|
+
L.poner(x, 0, parte, { c: color, b: true }); x += [...parte].length;
|
|
105
|
+
}
|
|
106
|
+
L.poner(2, 1, '═'.repeat(ANCHO - 4), { c: COLOR.tenue });
|
|
107
|
+
if (esEjemplo) L.centro(0, ANCHO, 3, 'EJEMPLO · datos ficticios para ver el diseño', { c: '#facc15', b: true });
|
|
108
|
+
const leyenda = [];
|
|
109
|
+
if (s.lider.modelo) leyenda.push([`${lider.toLowerCase()} · líder`, cLider]);
|
|
110
|
+
if (enjambre) leyenda.push([`${enjambre.toLowerCase()} · enjambre`, cEnjambre]);
|
|
111
|
+
leyenda.push(['delegación', COLOR.ok], ['asesor', COLOR.opus]);
|
|
112
|
+
const revision = (s.revisiones ?? []).at(-1);
|
|
113
|
+
if (revision) leyenda.push([`${revision.ia} · revisa`, colorIA(revision.ia)]);
|
|
114
|
+
x = 4;
|
|
115
|
+
for (const [txt, color] of leyenda) { L.poner(x, 2, '■', { c: color }); L.poner(x + 2, 2, txt, { c: COLOR.texto }); x += [...txt].length + 7; }
|
|
116
|
+
|
|
117
|
+
// ---- Líder
|
|
118
|
+
const lx = CX - LIDER.w / 2;
|
|
119
|
+
L.caja(lx, LIDER.y, LIDER.w, LIDER.h, cLider);
|
|
120
|
+
L.centro(lx + 1, LIDER.w - 2, LIDER.y + 1, `${lider} · sesión principal`, { c: cLider, b: true });
|
|
121
|
+
L.centro(lx + 1, LIDER.w - 2, LIDER.y + 2, `pasos ${s.pasos} · herramientas ${s.lider.herramientas ?? 0}`, { c: COLOR.texto });
|
|
122
|
+
L.centro(lx + 1, LIDER.w - 2, LIDER.y + 3, `leídos ${tokens(s.lider.leidos)} · escritos ${tokens(s.lider.escritos)}`, { c: COLOR.texto });
|
|
123
|
+
const esf = `esfuerzo ${barraEsfuerzo(s.lider.esfuerzo)} ${s.lider.esfuerzo ?? ''}`.trimEnd();
|
|
124
|
+
L.centro(lx + 1, LIDER.w - 2, LIDER.y + 4, esf, { c: COLOR.texto });
|
|
125
|
+
L.centro(lx + 1, LIDER.w - 2, LIDER.y + 5, `--model ${alias(s.lider.modelo)}`, { c: COLOR.tenue });
|
|
126
|
+
|
|
127
|
+
// ---- Revisión externa (a la derecha del líder)
|
|
128
|
+
if (revision) {
|
|
129
|
+
const c = colorIA(revision.ia);
|
|
130
|
+
L.poner(lx + LIDER.w, LIDER.y + 3, '▶', { c });
|
|
131
|
+
L.caja(REVISION.x, LIDER.y, REVISION.w, REVISION.h, c);
|
|
132
|
+
const dentro = (y, txt, estilo) => L.centro(REVISION.x + 1, REVISION.w - 2, y, txt, estilo);
|
|
133
|
+
dentro(LIDER.y + 1, 'REVISIÓN', { c, b: true });
|
|
134
|
+
dentro(LIDER.y + 2, NOMBRE_IA[revision.ia] ?? revision.ia, { c });
|
|
135
|
+
dentro(LIDER.y + 3, revision.respondio ?? revision.modelo ?? 'modelo ?', { c: COLOR.texto });
|
|
136
|
+
dentro(LIDER.y + 4, revision.esfuerzo ? `esfuerzo ${revision.esfuerzo}` : '', { c: COLOR.tenue });
|
|
137
|
+
const t = duracion((revision.fin ?? ahora) - revision.inicio);
|
|
138
|
+
const est = revision.estado === 'lista' ? [`✓ lista · ${t}`, COLOR.ok] : revision.estado === 'error' ? ['✗ falló', COLOR.error] : [`◐ revisando ${t}`, c];
|
|
139
|
+
dentro(LIDER.y + 5, est[0], { c: est[1] });
|
|
140
|
+
const total = (revision.entrada ?? 0) + (revision.salida ?? 0);
|
|
141
|
+
dentro(LIDER.y + 6, total ? `tokens ${tokens(total)}` : 'tokens —', { c: COLOR.tenue });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- Delegación
|
|
145
|
+
const dx = CX - DELEGACION.w / 2;
|
|
146
|
+
L.poner(CX, LIDER.y + LIDER.h, '┊', { c: COLOR.tenue });
|
|
147
|
+
L.caja(dx, DELEGACION.y, DELEGACION.w, DELEGACION.h, COLOR.ok);
|
|
148
|
+
L.poner(dx + 2, DELEGACION.y + 1, 'DELEGACIÓN · subagentes', { c: COLOR.ok, b: true });
|
|
149
|
+
L.poner(dx + DELEGACION.w - 3 - String(r.total).length, DELEGACION.y + 1, String(r.total), { c: COLOR.texto, b: true });
|
|
150
|
+
const filaD = (y, nombre, n, color) => {
|
|
151
|
+
L.poner(dx + 2, y, nombre, { c: COLOR.texto });
|
|
152
|
+
L.poner(dx + 15, y, barra(r.total ? n / r.total : 0, 22), { c: color });
|
|
153
|
+
L.poner(dx + 39, y, `${n}/${r.total}`, { c: COLOR.texto });
|
|
154
|
+
};
|
|
155
|
+
filaD(DELEGACION.y + 2, 'listos', r.listos, COLOR.ok);
|
|
156
|
+
filaD(DELEGACION.y + 3, 'corriendo', r.corriendo, cEnjambre);
|
|
157
|
+
filaD(DELEGACION.y + 4, 'con error', r.errores, COLOR.error);
|
|
158
|
+
const tokSub = s.agentes.reduce((n, a) => n + a.leidos, 0);
|
|
159
|
+
L.poner(dx + 2, DELEGACION.y + 5, `herramientas ${r.herramientas} · tokens leídos ${tokens(tokSub)}`, { c: COLOR.ok });
|
|
160
|
+
|
|
161
|
+
// ---- Delegar y subagentes
|
|
162
|
+
const dy = DELEGACION.y + DELEGACION.h;
|
|
163
|
+
L.poner(CX, dy, '┊', { c: COLOR.tenue });
|
|
164
|
+
const frase = `delegar a subagentes · ${enjambre ?? '—'}`;
|
|
165
|
+
const fx = CX - Math.floor([...frase].length / 2);
|
|
166
|
+
L.poner(fx, dy + 1, 'delegar a subagentes · ', { c: COLOR.texto });
|
|
167
|
+
L.poner(fx + 'delegar a subagentes · '.length, dy + 1, enjambre ?? '—', { c: cEnjambre, b: true });
|
|
168
|
+
const visibles = s.agentes.slice(-3);
|
|
169
|
+
const centros = SUB.xs.slice(0, Math.max(1, visibles.length)).map((x1) => x1 + SUB.w / 2);
|
|
170
|
+
const by = SUB.y - 2;
|
|
171
|
+
if (visibles.length > 1) {
|
|
172
|
+
L.poner(centros[0], by, `┌${'─'.repeat(centros.at(-1) - centros[0] - 1)}┐`, { c: COLOR.tenue });
|
|
173
|
+
L.poner(CX, by, centros.includes(CX) ? '┴' : '┴', { c: COLOR.tenue });
|
|
174
|
+
} else {
|
|
175
|
+
L.poner(centros[0], by, '┊', { c: COLOR.tenue });
|
|
176
|
+
}
|
|
177
|
+
centros.forEach((cx, i) => L.poner(cx, by + 1, i === 1 ? '●' : '▼', { c: cEnjambre }));
|
|
178
|
+
const totalSub = visibles.reduce((n, a) => n + a.leidos, 0);
|
|
179
|
+
visibles.forEach((a, i) => {
|
|
180
|
+
const x1 = SUB.xs[i];
|
|
181
|
+
const c = colorDe(a.modelo);
|
|
182
|
+
L.caja(x1, SUB.y, SUB.w, SUB.h, c);
|
|
183
|
+
const dentro = (y, txt, estilo) => L.centro(x1 + 1, SUB.w - 2, y, txt, estilo);
|
|
184
|
+
dentro(SUB.y + 1, a.nombre, { c: COLOR.texto, b: true });
|
|
185
|
+
dentro(SUB.y + 2, etiquetaModelo(a.modelo), { c });
|
|
186
|
+
const parte = totalSub ? a.leidos / totalSub : 0;
|
|
187
|
+
L.poner(x1 + 2, SUB.y + 3, barra(parte, 13), { c, d: a.estado === 'corriendo' && parte === 0 });
|
|
188
|
+
const pct = `${Math.round(parte * 100)}%`;
|
|
189
|
+
L.poner(x1 + SUB.w - 2 - pct.length, SUB.y + 3, pct, { c: COLOR.texto });
|
|
190
|
+
dentro(SUB.y + 4, a.ultimo || '—', { c: COLOR.texto });
|
|
191
|
+
const est = a.estado === 'listo' ? ['✓ listo', COLOR.ok] : a.estado === 'error' ? ['✗ con error', COLOR.error] : ['◐ corriendo', c];
|
|
192
|
+
dentro(SUB.y + 5, est[0], { c: est[1] });
|
|
193
|
+
dentro(SUB.y + 6, `leídos ${tokens(a.leidos)} · ${duracion((a.fin ?? ahora) - a.inicio)}`, { c: COLOR.tenue });
|
|
194
|
+
});
|
|
195
|
+
if (s.agentes.length > visibles.length) L.poner(SUB.xs[0], SUB.y + SUB.h, `+${s.agentes.length - visibles.length} más`, { c: COLOR.tenue });
|
|
196
|
+
|
|
197
|
+
// ---- Vuelta al líder
|
|
198
|
+
const my = SUB.y + SUB.h;
|
|
199
|
+
if (visibles.length > 1) {
|
|
200
|
+
centros.forEach((cx) => L.poner(cx, my, '│', { c: COLOR.tenue }));
|
|
201
|
+
L.poner(centros[0], my + 1, `└${'─'.repeat(centros.at(-1) - centros[0] - 1)}┘`, { c: COLOR.tenue });
|
|
202
|
+
L.poner(CX, my + 1, '●', { c: COLOR.texto });
|
|
203
|
+
} else {
|
|
204
|
+
L.poner(centros[0], my, '┊', { c: COLOR.tenue });
|
|
205
|
+
}
|
|
206
|
+
L.poner(CX, my + 2, '┊', { c: COLOR.tenue });
|
|
207
|
+
const vx = CX - VUELTA.w / 2;
|
|
208
|
+
L.caja(vx, VUELTA.y, VUELTA.w, VUELTA.h, cLider);
|
|
209
|
+
L.centro(vx + 1, VUELTA.w - 2, VUELTA.y + 1, `de vuelta en ${lider} · líder`, { c: cLider, b: true });
|
|
210
|
+
L.centro(vx + 1, VUELTA.w - 2, VUELTA.y + 2, r.corriendo > 0 ? `esperando ${r.corriendo} ${r.corriendo === 1 ? 'subagente' : 'subagentes'}` : 'subagentes terminados', { c: COLOR.texto });
|
|
211
|
+
|
|
212
|
+
// ---- Asesor (columna izquierda, de arriba a la vuelta al líder)
|
|
213
|
+
const ay = LIDER.y;
|
|
214
|
+
const ah = VUELTA.y + VUELTA.h - ay;
|
|
215
|
+
L.caja(ASESOR.x, ay, ASESOR.w, ah, COLOR.opus);
|
|
216
|
+
L.centro(ASESOR.x + 1, ASESOR.w - 2, ay + 1, asesorModelo ? etiquetaModelo(asesorModelo).toUpperCase() : 'ASESOR', { c: COLOR.opus, b: true });
|
|
217
|
+
L.centro(ASESOR.x + 1, ASESOR.w - 2, ay + 2, s.asesor.llamadas > 0 ? 'asesor · consultado' : 'asesor · en espera', { c: COLOR.opus });
|
|
218
|
+
const consultas = s.registro.filter((e) => e.quien === 'asesor').slice(-6);
|
|
219
|
+
const primera = LIDER.y + 3;
|
|
220
|
+
const ultima = VUELTA.y + 1;
|
|
221
|
+
const filasC = consultas.length <= 1 ? [primera]
|
|
222
|
+
: consultas.map((_, i) => (i === 0 ? primera : i === consultas.length - 1 ? ultima : Math.round(primera + 5 + ((ultima - primera - 10) * i) / (consultas.length - 1))));
|
|
223
|
+
const ocupadas = new Set(filasC.flatMap((y) => [y, y + 1]));
|
|
224
|
+
if (consultas.length > 1) for (let y = filasC[0] + 1; y < filasC.at(-1); y++) if (!ocupadas.has(y)) L.poner(ASESOR.x + 2, y, '┊', { c: COLOR.tenue });
|
|
225
|
+
consultas.forEach((c, i) => {
|
|
226
|
+
const y = filasC[i];
|
|
227
|
+
const final = i === consultas.length - 1;
|
|
228
|
+
const estilo = final ? { c: COLOR.opus, b: true, bg: COLOR.resalte } : { c: COLOR.texto };
|
|
229
|
+
if (final) L.poner(ASESOR.x + 1, y, ' '.repeat(ASESOR.w - 2), { bg: COLOR.resalte });
|
|
230
|
+
L.poner(ASESOR.x + 2, y, `${final ? '◆' : '◇'} ${hora(c.t)} consulta`, estilo);
|
|
231
|
+
if (c.ms) L.poner(ASESOR.x + 4, y + 1, `duró ${duracion(c.ms)}`, { c: COLOR.tenue });
|
|
232
|
+
});
|
|
233
|
+
if (consultas.length > 0) {
|
|
234
|
+
L.poner(ASESOR.x + ASESOR.w + 1, filasC[0], `${'┈'.repeat(lx - (ASESOR.x + ASESOR.w + 1) - 2)}▶`, { c: COLOR.tenue });
|
|
235
|
+
if (consultas.length > 1) L.poner(ASESOR.x + ASESOR.w + 1, filasC.at(-1), `${'┈'.repeat(vx - (ASESOR.x + ASESOR.w + 1) - 2)}▶`, { c: COLOR.tenue });
|
|
236
|
+
} else {
|
|
237
|
+
L.centro(ASESOR.x + 1, ASESOR.w - 2, primera, 'sin consultas', { c: COLOR.tenue });
|
|
238
|
+
}
|
|
239
|
+
const sy = ay + ah - 5;
|
|
240
|
+
L.poner(ASESOR.x + 2, sy, 'consultas', { c: COLOR.texto });
|
|
241
|
+
L.poner(ASESOR.x + ASESOR.w - 3 - String(s.asesor.llamadas).length, sy, String(s.asesor.llamadas), { c: COLOR.opus, b: true });
|
|
242
|
+
L.poner(ASESOR.x + 2, sy + 1, 'tiempo', { c: COLOR.texto });
|
|
243
|
+
const t = duracion(s.asesor.tiempo ?? 0);
|
|
244
|
+
L.poner(ASESOR.x + ASESOR.w - 2 - [...t].length, sy + 1, t, { c: COLOR.opus });
|
|
245
|
+
|
|
246
|
+
// ---- Registro de sesión
|
|
247
|
+
const ry = REGISTRO_Y;
|
|
248
|
+
L.caja(2, ry, ANCHO - 4, lineasRegistro + 2, COLOR.tenue, 'registro de sesión');
|
|
249
|
+
const quienColor = (q) => (q === 'líder' ? cLider : q === 'asesor' ? COLOR.opus : colorDe(s.agentes.find((a) => a.nombre === q)?.modelo));
|
|
250
|
+
s.registro.slice(-lineasRegistro).forEach((e, i) => {
|
|
251
|
+
const y = ry + 1 + i;
|
|
252
|
+
L.poner(4, y, hora(e.t), { c: COLOR.tenue });
|
|
253
|
+
L.poner(14, y, L.corto(e.quien, 14), { c: quienColor(e.quien), b: true });
|
|
254
|
+
const [herramienta, ...resto] = e.texto.split(' · ');
|
|
255
|
+
if (resto.length && /^[A-Z]/.test(herramienta) && !/^(empieza|terminó|consultado)/.test(e.texto)) {
|
|
256
|
+
L.poner(30, y, L.corto(herramienta, 14), { c: COLOR.texto });
|
|
257
|
+
L.poner(46, y, `→ ${L.corto(resto.join(' · '), 52)}`, { c: COLOR.texto });
|
|
258
|
+
} else {
|
|
259
|
+
L.poner(30, y, L.corto(e.texto, 68), { c: COLOR.texto });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ---- Orden y estado
|
|
264
|
+
const oy = ry + lineasRegistro + 3;
|
|
265
|
+
const prefijo = `~/${carpeta || ''} $ `.replace('~/ $', '~ $');
|
|
266
|
+
L.poner(2, oy, prefijo, { c: COLOR.tenue });
|
|
267
|
+
L.poner(2 + [...prefijo].length, oy, `claude --model ${alias(s.lider.modelo)}${asesorModelo ? ` --advisor ${alias(asesorModelo)}` : ''}`, { c: COLOR.texto, b: true });
|
|
268
|
+
x = 2;
|
|
269
|
+
for (const etiqueta of estadoPorFamilia(s)) {
|
|
270
|
+
const m = etiqueta.match(/^(.*?) \[(.*)\]$/u);
|
|
271
|
+
L.poner(x, oy + 1, `${m[1]} `, { c: COLOR.tenue }); x += m[1].length + 1;
|
|
272
|
+
const c = /enjambre/.test(m[2]) ? cEnjambre : /consultado/.test(m[2]) ? COLOR.opus : /error/.test(m[2]) ? COLOR.error : COLOR.ok;
|
|
273
|
+
L.poner(x, oy + 1, `[${m[2]}]`, { c, b: true }); x += m[2].length + 2 + 3;
|
|
274
|
+
}
|
|
275
|
+
let ly = oy + 2;
|
|
276
|
+
if (medidores.length) pintarMedidores(L, ly++, medidores);
|
|
277
|
+
if (lineaModelo) {
|
|
278
|
+
L.poner(2, ly, lineaModelo.actual, { c: COLOR.texto });
|
|
279
|
+
L.poner(2 + [...lineaModelo.actual].length, ly, lineaModelo.cola, { c: lineaModelo.coincide ? COLOR.ok : '#facc15', b: !lineaModelo.coincide });
|
|
280
|
+
ly++;
|
|
281
|
+
}
|
|
282
|
+
if (codex) pintarCodex(L, ly, codex, ahora);
|
|
283
|
+
|
|
284
|
+
return { filas: aTramos(L.celdas), ancho: ANCHO, alto };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- Uso y modelo, como las líneas de estado de nexakit-sesion y nexakit-modelo, pero con barras
|
|
288
|
+
const esNumero = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
289
|
+
export function medidoresDeUso(uso) {
|
|
290
|
+
const m = [];
|
|
291
|
+
if (esNumero(uso?.context?.percent)) m.push(['contexto', uso.context.percent]);
|
|
292
|
+
for (const l of uso?.rateLimits ?? []) {
|
|
293
|
+
if (!esNumero(l?.percentUsed)) continue;
|
|
294
|
+
if (l.kind === 'five_hour') m.push(['5 h', l.percentUsed]);
|
|
295
|
+
if (l.kind === 'seven_day') m.push(['semana', l.percentUsed]);
|
|
296
|
+
}
|
|
297
|
+
return m;
|
|
298
|
+
}
|
|
299
|
+
const colorDeNivel = (p) => (p >= 85 ? COLOR.error : p >= 60 ? '#facc15' : COLOR.ok);
|
|
300
|
+
function pintarMedidores(L, y, medidores) {
|
|
301
|
+
let x = 2;
|
|
302
|
+
for (const [nombre, p] of medidores) {
|
|
303
|
+
L.poner(x, y, nombre, { c: COLOR.tenue }); x += [...nombre].length + 1;
|
|
304
|
+
L.poner(x, y, barra(p / 100, 10), { c: colorDeNivel(p) }); x += 11;
|
|
305
|
+
const pct = `${Math.round(p)} %`;
|
|
306
|
+
L.poner(x, y, pct, { c: COLOR.texto, b: true }); x += pct.length + 4;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const hace = (ms) => (ms < 60_000 ? `hace ${Math.max(0, Math.round(ms / 1000))} s` : ms < 3_600_000 ? `hace ${Math.round(ms / 60_000)} min` : `hace ${Math.round(ms / 3_600_000)} h`);
|
|
310
|
+
// Uso de Codex leído de su propia sesión: tokens, límites y cuándo fue la última actividad.
|
|
311
|
+
function pintarCodex(L, y, codex, ahora) {
|
|
312
|
+
let x = 2;
|
|
313
|
+
const cabeza = `codex ${codex.modelo ?? '?'}${codex.esfuerzo ? ` · ${codex.esfuerzo}` : ''}`;
|
|
314
|
+
L.poner(x, y, cabeza, { c: COLOR.codex, b: true }); x += [...cabeza].length + 2;
|
|
315
|
+
const tok = `leídos ${tokens(codex.leidos)} · escritos ${tokens(codex.escritos)}`;
|
|
316
|
+
L.poner(x, y, tok, { c: COLOR.texto }); x += [...tok].length + 2;
|
|
317
|
+
for (const [nombre, p] of [['5 h', codex.limite5h], ['semana', codex.limiteSemana]]) {
|
|
318
|
+
if (!esNumero(p)) continue;
|
|
319
|
+
L.poner(x, y, nombre, { c: COLOR.tenue }); x += [...nombre].length + 1;
|
|
320
|
+
L.poner(x, y, barra(p / 100, 5), { c: colorDeNivel(p) }); x += 6;
|
|
321
|
+
const pct = `${Math.round(p)} %`;
|
|
322
|
+
L.poner(x, y, pct, { c: COLOR.texto, b: true }); x += pct.length + 2;
|
|
323
|
+
}
|
|
324
|
+
if (esNumero(codex.actualizado)) L.poner(x, y, hace(ahora - codex.actualizado), { c: COLOR.tenue });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function modeloFrenteARecomendado(s) {
|
|
328
|
+
const actual = `modelo ${s.lider.modelo} · ${s.lider.esfuerzo ?? 'esfuerzo ?'}`;
|
|
329
|
+
const rec = s.recomendacion;
|
|
330
|
+
if (!rec) return { actual, cola: '', coincide: true };
|
|
331
|
+
const coincide = familia(s.lider.modelo) === familia(rec.modelo) && s.lider.esfuerzo === rec.esfuerzo;
|
|
332
|
+
return { actual, cola: coincide ? ' ✓' : ` → recomendado: ${rec.modelo} · ${rec.esfuerzo}`, coincide };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Para el panel de Claude: los espacios sin estilo se pegan al tramo vecino, para no dejar textos solo de espacios.
|
|
336
|
+
export function tramosParaPanel(filas) {
|
|
337
|
+
return filas.map((tramos) => {
|
|
338
|
+
const salida = [];
|
|
339
|
+
let pendiente = '';
|
|
340
|
+
for (const t of tramos) {
|
|
341
|
+
const soloEspacio = /^ +$/u.test(t.t) && !t.bg && !t.c;
|
|
342
|
+
if (soloEspacio) {
|
|
343
|
+
if (salida.length) salida.at(-1).t += t.t;
|
|
344
|
+
else pendiente += t.t;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
salida.push({ ...t, t: pendiente + t.t });
|
|
348
|
+
pendiente = '';
|
|
349
|
+
}
|
|
350
|
+
if (pendiente) salida.push({ t: pendiente });
|
|
351
|
+
return salida;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---- A ANSI para la terminal
|
|
356
|
+
const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
357
|
+
const a256 = ([r, g, b]) => 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
|
|
358
|
+
const a16 = ([r, g, b]) => {
|
|
359
|
+
const brillo = (r + g + b) / 3 > 140 ? 60 : 0;
|
|
360
|
+
return 30 + brillo + (r > 127 ? 1 : 0) + (g > 127 ? 2 : 0) + (b > 127 ? 4 : 0);
|
|
361
|
+
};
|
|
362
|
+
function sgr(tramo, nivel) {
|
|
363
|
+
const partes = [];
|
|
364
|
+
if (tramo.b) partes.push('1');
|
|
365
|
+
if (tramo.d) partes.push('2');
|
|
366
|
+
if (tramo.c) {
|
|
367
|
+
const c = rgb(tramo.c);
|
|
368
|
+
partes.push(nivel === 'truecolor' ? `38;2;${c.join(';')}` : nivel === '256' ? `38;5;${a256(c)}` : String(a16(c)));
|
|
369
|
+
}
|
|
370
|
+
if (tramo.bg) {
|
|
371
|
+
const c = rgb(tramo.bg);
|
|
372
|
+
partes.push(nivel === 'truecolor' ? `48;2;${c.join(';')}` : nivel === '256' ? `48;5;${a256(c)}` : String(a16(c) + 10));
|
|
373
|
+
}
|
|
374
|
+
return partes.join(';');
|
|
375
|
+
}
|
|
376
|
+
export function aAnsi(filas, nivel) {
|
|
377
|
+
return filas.map((tramos) => tramos.map((t) => {
|
|
378
|
+
if (nivel === 'none') return t.t;
|
|
379
|
+
const codigo = sgr(t, nivel);
|
|
380
|
+
return codigo ? `\x1b[${codigo}m${t.t}\x1b[0m` : t.t;
|
|
381
|
+
}).join('')).join('\n');
|
|
382
|
+
}
|