helix-assistant-cli 0.2.14 → 0.6.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/README.md +25 -0
- package/dist/commands/browser.js +3 -2
- package/dist/commands/memory.d.ts +6 -0
- package/dist/commands/memory.js +129 -0
- package/dist/commands/model.d.ts +24 -0
- package/dist/commands/model.js +220 -0
- package/dist/commands/subagents.d.ts +15 -0
- package/dist/commands/subagents.js +123 -0
- package/dist/config/mcp-defaults.js +13 -4
- package/dist/index.js +5 -1
- package/dist/ui/autocomplete.js +23 -8
- package/dist/ui/browser-ascii.d.ts +2 -0
- package/dist/ui/browser-ascii.js +67 -0
- package/dist/ui/repl.d.ts +1 -0
- package/dist/ui/repl.js +89 -42
- package/package.json +4 -2
- package/src/commands/browser.ts +3 -2
- package/src/commands/memory.ts +167 -0
- package/src/commands/model.ts +237 -0
- package/src/commands/subagents.ts +150 -0
- package/src/config/mcp-defaults.ts +13 -4
- package/src/global.d.ts +23 -0
- package/src/index.ts +2 -1
- package/src/ui/autocomplete.ts +26 -9
- package/src/ui/browser-ascii.ts +82 -0
- package/src/ui/repl.ts +101 -43
- package/tests/cli.test.ts +41 -0
- package/tsconfig.json +4 -4
package/src/ui/autocomplete.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
1
|
+
import chalk from 'chalk';
|
|
2
2
|
import type { AgentClient } from '../client/agent-client';
|
|
3
3
|
import { formatSkill, formatMcp, ORANGE, CYAN, MAGENTA, DIM } from './colors';
|
|
4
4
|
import { discoverUserSkillNames, getUserSkillsDirectory } from '../skills/user-skill-discovery';
|
|
@@ -14,16 +14,19 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
|
|
|
14
14
|
{ command: '/skill', description: 'Listar o inspeccionar skills registradas', usage: '/skill [nombre]' },
|
|
15
15
|
{ command: '/mcp', description: 'Listar servidores MCP y sus herramientas', usage: '/mcp [defaults]' },
|
|
16
16
|
{ command: '/create-mcp', description: 'Registrar y configurar un nuevo servidor MCP' },
|
|
17
|
-
{ command: '/browser', description: 'Ver estado
|
|
17
|
+
{ command: '/browser', description: 'Ver estado, abrir URLs o alternar la vista ASCII', usage: '/browser [open <url>|live [on|off]]' },
|
|
18
|
+
{ command: '/subagents', description: 'Ver el panel ASCII o chatear con un subagente', usage: '/subagents [list|chat <id> <mensaje>]' },
|
|
18
19
|
{ command: '/config-apikey', description: 'Configurador interactivo de API Keys (OpenAI, Anthropic, Gemini, etc.)' },
|
|
19
20
|
{ command: '/show-apikey', description: 'Ver estado y validez de las API Keys configuradas' },
|
|
20
|
-
{ command: '/microphone', description: 'Ver o alternar estado del
|
|
21
|
+
{ command: '/microphone', description: 'Ver o alternar estado del micrófono', usage: '/microphone [enable|disable]' },
|
|
21
22
|
{ command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
|
|
23
|
+
{ command: '/model', description: 'Ver o cambiar el modelo activo (free/local/cloud)', usage: '/model [list|set <id>|effort <low|medium|high>]' },
|
|
22
24
|
{ command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
|
|
25
|
+
{ command: '/memory', description: 'Ver estadísticas del harness de aprendizaje adaptativo', usage: '/memory [stats|rules|reset]' },
|
|
23
26
|
{ command: '/status', description: 'Mostrar resumen del estado del sistema' },
|
|
24
27
|
{ command: '/providers', description: 'Ver proveedores configurados y salud de sus claves' },
|
|
25
28
|
{ command: '/capabilities', description: 'Ver capacidades activas: sandbox, subagentes, voz y browser' },
|
|
26
|
-
{ command: '/settings', description: 'Ver
|
|
29
|
+
{ command: '/settings', description: 'Ver configuración efectiva sin exponer secretos' },
|
|
27
30
|
{ command: '/clear', description: 'Limpiar pantalla y restaurar el banner de HELIX' },
|
|
28
31
|
{ command: '/stop', description: 'Parada de emergencia de todas las tareas activas' },
|
|
29
32
|
{ command: '/help', description: 'Mostrar ayuda de comandos y atajos' },
|
|
@@ -116,7 +119,21 @@ export class Autocompleter {
|
|
|
116
119
|
return [hits.length ? hits : options, line];
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// 3.
|
|
122
|
+
// 3. Model sub-arguments
|
|
123
|
+
if (trimmed.startsWith('/model ')) {
|
|
124
|
+
const options = ['/model list', '/model set ', '/model effort low', '/model effort medium', '/model effort high'];
|
|
125
|
+
const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
126
|
+
return [hits.length ? hits : options, line];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3b. Memory sub-arguments
|
|
130
|
+
if (trimmed.startsWith('/memory ')) {
|
|
131
|
+
const options = ['/memory stats', '/memory rules', '/memory reset'];
|
|
132
|
+
const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
133
|
+
return [hits.length ? hits : options, line];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 4. Browser sub-arguments
|
|
120
137
|
if (trimmed.startsWith('/browser ')) {
|
|
121
138
|
const options = ['/browser open https://'];
|
|
122
139
|
return [options, line];
|
|
@@ -158,16 +175,16 @@ export class Autocompleter {
|
|
|
158
175
|
*/
|
|
159
176
|
public renderHelp(): void {
|
|
160
177
|
console.log('');
|
|
161
|
-
console.log(` ${chalk.hex('#FF8C00').bold('
|
|
162
|
-
console.log(` ${chalk.gray('
|
|
178
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ Comandos Disponibles')} ${chalk.gray('(/ + [Tab] para autocompletar)')}`);
|
|
179
|
+
console.log(` ${chalk.gray('─'.repeat(50))}`);
|
|
163
180
|
for (const cmd of SLASH_COMMANDS) {
|
|
164
181
|
const usage = cmd.usage ? ` ${chalk.gray(cmd.usage)}` : '';
|
|
165
182
|
console.log(` ${chalk.magenta(cmd.command.padEnd(16))} ${chalk.gray(cmd.description)}${usage}`);
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
console.log('');
|
|
169
|
-
console.log(` ${chalk.hex('#FF8C00').bold('
|
|
170
|
-
console.log(` ${chalk.gray('
|
|
186
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ Prefijos Especiales')}`);
|
|
187
|
+
console.log(` ${chalk.gray('─'.repeat(50))}`);
|
|
171
188
|
console.log(` ${formatSkill('nombre')} ${chalk.gray('Invoca una skill registrada')}`);
|
|
172
189
|
console.log(` ${formatMcp('herramienta')} ${chalk.gray('Invoca herramientas MCP')}`);
|
|
173
190
|
console.log('');
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import jpeg from 'jpeg-js';
|
|
2
|
+
|
|
3
|
+
const ASCII_RAMP = ' .:-=+*#%@';
|
|
4
|
+
const MAX_FRAME_WIDTH = 76;
|
|
5
|
+
const MAX_FRAME_HEIGHT = 34;
|
|
6
|
+
const MAX_JPEG_BYTES = 560_000;
|
|
7
|
+
|
|
8
|
+
export function grayscaleToAscii(
|
|
9
|
+
pixels: number[][],
|
|
10
|
+
width = MAX_FRAME_WIDTH,
|
|
11
|
+
): string {
|
|
12
|
+
if (!pixels.length || !pixels[0]?.length) {
|
|
13
|
+
throw new Error('Browser frame has no pixels to render.');
|
|
14
|
+
}
|
|
15
|
+
const boundedWidth = Math.max(1, Math.min(MAX_FRAME_WIDTH, Math.floor(width)));
|
|
16
|
+
const sourceHeight = pixels.length;
|
|
17
|
+
const sourceWidth = pixels[0].length;
|
|
18
|
+
const outputHeight = Math.max(
|
|
19
|
+
1,
|
|
20
|
+
Math.min(MAX_FRAME_HEIGHT, Math.round((sourceHeight / sourceWidth) * boundedWidth * 0.5)),
|
|
21
|
+
);
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (let y = 0; y < outputHeight; y += 1) {
|
|
25
|
+
const sourceY = Math.min(sourceHeight - 1, Math.floor((y / outputHeight) * sourceHeight));
|
|
26
|
+
let line = '';
|
|
27
|
+
for (let x = 0; x < boundedWidth; x += 1) {
|
|
28
|
+
const sourceX = Math.min(sourceWidth - 1, Math.floor((x / boundedWidth) * sourceWidth));
|
|
29
|
+
const value = Math.max(0, Math.min(255, pixels[sourceY][sourceX]));
|
|
30
|
+
const index = Math.round(((255 - value) / 255) * (ASCII_RAMP.length - 1));
|
|
31
|
+
line += ASCII_RAMP[index];
|
|
32
|
+
}
|
|
33
|
+
lines.push(line);
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderJpegAsAscii(base64: string, width = MAX_FRAME_WIDTH): string {
|
|
39
|
+
if (
|
|
40
|
+
typeof base64 !== 'string'
|
|
41
|
+
|| !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)
|
|
42
|
+
|| base64.length > Math.ceil(MAX_JPEG_BYTES * 4 / 3)
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('Browser frame is not a valid, bounded Base64 JPEG.');
|
|
45
|
+
}
|
|
46
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
47
|
+
if (bytes.length === 0 || bytes.length > MAX_JPEG_BYTES) {
|
|
48
|
+
throw new Error('Browser frame exceeds the supported JPEG size.');
|
|
49
|
+
}
|
|
50
|
+
const image = jpeg.decode(bytes, {
|
|
51
|
+
useTArray: true,
|
|
52
|
+
formatAsRGBA: true,
|
|
53
|
+
maxResolutionInMP: 4,
|
|
54
|
+
maxMemoryUsageInMB: 128,
|
|
55
|
+
});
|
|
56
|
+
const pixels: number[][] = [];
|
|
57
|
+
const outputWidth = Math.min(MAX_FRAME_WIDTH, image.width);
|
|
58
|
+
const outputHeight = Math.max(
|
|
59
|
+
1,
|
|
60
|
+
Math.min(
|
|
61
|
+
MAX_FRAME_HEIGHT,
|
|
62
|
+
Math.round((image.height / image.width) * outputWidth * 0.5),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (let y = 0; y < outputHeight; y += 1) {
|
|
67
|
+
const row: number[] = [];
|
|
68
|
+
const sourceY = Math.min(image.height - 1, Math.floor((y / outputHeight) * image.height));
|
|
69
|
+
for (let x = 0; x < outputWidth; x += 1) {
|
|
70
|
+
const sourceX = Math.min(image.width - 1, Math.floor((x / outputWidth) * image.width));
|
|
71
|
+
const offset = (sourceY * image.width + sourceX) * 4;
|
|
72
|
+
row.push(
|
|
73
|
+
image.data[offset] * 0.299
|
|
74
|
+
+ image.data[offset + 1] * 0.587
|
|
75
|
+
+ image.data[offset + 2] * 0.114,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
pixels.push(row);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return grayscaleToAscii(pixels, Math.min(width, outputWidth));
|
|
82
|
+
}
|
package/src/ui/repl.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
1
|
+
import readline from 'readline';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import prompts from 'prompts';
|
|
4
4
|
import { AgentClient } from '../client/agent-client';
|
|
@@ -24,9 +24,13 @@ import { handleCreateSkill, handleListSkills } from '../commands/skills';
|
|
|
24
24
|
import { handleCreateMcp, handleListMcp, handleMcpDefaults } from '../commands/mcp';
|
|
25
25
|
import { handleMicrophoneCommand } from '../commands/mic';
|
|
26
26
|
import { handleBrowserCommand } from '../commands/browser';
|
|
27
|
+
import { handleSubagentsCommand } from '../commands/subagents';
|
|
27
28
|
import type { ConfirmationRequest } from '../client/types';
|
|
28
29
|
import { formatTerminalMarkdown } from './markdown';
|
|
29
30
|
import { handleCapabilities, handleProviders, handleSettings } from '../commands/diagnostics';
|
|
31
|
+
import { renderJpegAsAscii } from './browser-ascii';
|
|
32
|
+
import { handleModelCommand } from '../commands/model';
|
|
33
|
+
import { handleMemoryCommand } from '../commands/memory';
|
|
30
34
|
|
|
31
35
|
export interface ReplOptions {
|
|
32
36
|
wsUrl: string;
|
|
@@ -47,6 +51,7 @@ export class HelixRepl {
|
|
|
47
51
|
private activeTasksCount: number = 0;
|
|
48
52
|
private isProcessingPrompt: boolean = false;
|
|
49
53
|
private plain: boolean = false;
|
|
54
|
+
private browserLiveViewEnabled: boolean = false;
|
|
50
55
|
|
|
51
56
|
constructor(private options: ReplOptions) {
|
|
52
57
|
this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
@@ -116,7 +121,7 @@ export class HelixRepl {
|
|
|
116
121
|
this.waitingLogged = false;
|
|
117
122
|
await this._syncSettings();
|
|
118
123
|
await this.autocompleter.refreshSuggestions();
|
|
119
|
-
process.stdout.write(`\r\x1b[K${chalk.green('
|
|
124
|
+
process.stdout.write(`\r\x1b[K${chalk.green('â—')} ${chalk.greenBright.bold('Conectado al agente HELIX')} \n`);
|
|
120
125
|
this._promptUser();
|
|
121
126
|
});
|
|
122
127
|
|
|
@@ -126,39 +131,39 @@ export class HelixRepl {
|
|
|
126
131
|
}
|
|
127
132
|
if (!this.waitingLogged) {
|
|
128
133
|
this.waitingLogged = true;
|
|
129
|
-
console.log(`\n${RED('
|
|
130
|
-
console.log(`${GRAY(' (Si no responde tras 3 reintentos, el CLI lo
|
|
134
|
+
console.log(`\n${RED('â—')} ${chalk.yellowBright('Esperando al servicio del agente HELIX en ws://127.0.0.1:8765...')}`);
|
|
135
|
+
console.log(`${GRAY(' (Si no responde tras 3 reintentos, el CLI lo iniciará automáticamente en segundo plano)')}\n`);
|
|
131
136
|
}
|
|
132
137
|
this._promptUser();
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
this.client.on('auto_spawning_agent', () => {
|
|
136
|
-
process.stdout.write(`\n${CYAN_BOLD('
|
|
137
|
-
process.stdout.write(`${GRAY(' Por favor espere mientras termina de inicializarse (no se
|
|
141
|
+
process.stdout.write(`\n${CYAN_BOLD('âš™ï¸')} ${chalk.hex('#FF9F1C').bold('[AUTO-START]:')} Iniciando el servicio del agente HELIX en segundo plano...\n`);
|
|
142
|
+
process.stdout.write(`${GRAY(' Por favor espere mientras termina de inicializarse (no se realizarán reintentos hasta que esté listo).')}\n`);
|
|
138
143
|
});
|
|
139
144
|
|
|
140
145
|
this.client.on('agent_starting_progress', (data: { elapsedSec: number }) => {
|
|
141
|
-
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('
|
|
146
|
+
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('â³')} ${chalk.gray(`Inicializando subsistemas de HELIX en segundo plano... (${data.elapsedSec}s)`)}`);
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
this.client.on('agent_ready', () => {
|
|
145
|
-
process.stdout.write(`\r\x1b[K ${chalk.green('
|
|
150
|
+
process.stdout.write(`\r\x1b[K ${chalk.green('✔')} ${chalk.greenBright('Servicio de HELIX listo. Conectando...')}\n\n`);
|
|
146
151
|
});
|
|
147
152
|
|
|
148
153
|
this.client.on('agent_start_failed', (data: { error?: string }) => {
|
|
149
|
-
process.stdout.write(`\r\x1b[K\n ${chalk.red('
|
|
154
|
+
process.stdout.write(`\r\x1b[K\n ${chalk.red('✖')} ${chalk.redBright('No se pudo iniciar el servicio automáticamente:')} ${chalk.gray(data?.error || 'Tiempo agotado')}\n`);
|
|
150
155
|
this._promptUser();
|
|
151
156
|
});
|
|
152
157
|
|
|
153
158
|
// Voice Capture indicator
|
|
154
159
|
this.client.on('voice_capture_started', () => {
|
|
155
160
|
this.isCapturingVoice = true;
|
|
156
|
-
process.stdout.write(`\n${chalk.bgRed.white.bold('
|
|
161
|
+
process.stdout.write(`\n${chalk.bgRed.white.bold(' ðŸŽ™ï¸ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
|
|
157
162
|
});
|
|
158
163
|
|
|
159
164
|
this.client.on('voice_capture_stopped', () => {
|
|
160
165
|
this.isCapturingVoice = false;
|
|
161
|
-
process.stdout.write(`\n${chalk.bgCyan.black.bold('
|
|
166
|
+
process.stdout.write(`\n${chalk.bgCyan.black.bold(' 🔊 [AUDIO RECIBIDO]: Procesando voz... ')}\n`);
|
|
162
167
|
this._promptUser();
|
|
163
168
|
});
|
|
164
169
|
|
|
@@ -166,7 +171,7 @@ export class HelixRepl {
|
|
|
166
171
|
this.client.on('react_step', (step: { iteration?: number; role?: string }) => {
|
|
167
172
|
const iter = step?.iteration ?? 1;
|
|
168
173
|
const role = step?.role ? chalk.gray(` (${step.role})`) : '';
|
|
169
|
-
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('
|
|
174
|
+
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray(`Paso ${iter}${role} — Razonando...`)}`);
|
|
170
175
|
});
|
|
171
176
|
|
|
172
177
|
// Agent assistant message
|
|
@@ -185,13 +190,13 @@ export class HelixRepl {
|
|
|
185
190
|
|
|
186
191
|
// Branded header
|
|
187
192
|
console.log('');
|
|
188
|
-
console.log(` ${chalk.hex('#FF8C00').bold('
|
|
193
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
|
|
189
194
|
|
|
190
195
|
// Content with subtle left border and clean Markdown formatting
|
|
191
196
|
const formatted = formatTerminalMarkdown(content, this.plain);
|
|
192
197
|
const contentLines = formatted.trim().split('\n');
|
|
193
198
|
for (const line of contentLines) {
|
|
194
|
-
console.log(` ${chalk.gray('
|
|
199
|
+
console.log(` ${chalk.gray('│')} ${line}`);
|
|
195
200
|
}
|
|
196
201
|
console.log('');
|
|
197
202
|
|
|
@@ -210,16 +215,39 @@ export class HelixRepl {
|
|
|
210
215
|
this.activeTaskId = task.task_id || null;
|
|
211
216
|
}
|
|
212
217
|
});
|
|
218
|
+
|
|
219
|
+
this.client.on('browser_frame', (payload: {
|
|
220
|
+
conversation_id?: string;
|
|
221
|
+
mime_type?: string;
|
|
222
|
+
image_base64?: string;
|
|
223
|
+
}) => {
|
|
224
|
+
if (
|
|
225
|
+
!this.browserLiveViewEnabled
|
|
226
|
+
|| !payload?.conversation_id?.startsWith('cli_')
|
|
227
|
+
|| payload.mime_type !== 'image/jpeg'
|
|
228
|
+
|| typeof payload.image_base64 !== 'string'
|
|
229
|
+
) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
process.stdout.write('\r\x1b[K');
|
|
234
|
+
console.log(`\n${CYAN_BOLD(' HELIX LIVE BROWSER')}`);
|
|
235
|
+
console.log(renderJpegAsAscii(payload.image_base64));
|
|
236
|
+
this._promptUser();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.log(RED(` No se pudo representar el cuadro del navegador: ${(error as Error).message}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
private async _handleConfirmationDialog(req: ConfirmationRequest): Promise<void> {
|
|
216
|
-
console.log(`\n${YELLOW_BOLD('
|
|
217
|
-
console.log(`
|
|
244
|
+
console.log(`\n${YELLOW_BOLD('âš ï¸ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
|
|
245
|
+
console.log(` Acción: ${chalk.white.bold(req.action || req.tool || 'Acción del sistema')}`);
|
|
218
246
|
if (req.description) {
|
|
219
|
-
console.log(`
|
|
247
|
+
console.log(` Descripción: ${DIM(req.description)}`);
|
|
220
248
|
}
|
|
221
249
|
if (req.arguments && Object.keys(req.arguments).length > 0) {
|
|
222
|
-
console.log(`
|
|
250
|
+
console.log(` Parámetros: ${GRAY(JSON.stringify(req.arguments))}`);
|
|
223
251
|
}
|
|
224
252
|
|
|
225
253
|
this.rl?.pause();
|
|
@@ -227,17 +255,17 @@ export class HelixRepl {
|
|
|
227
255
|
const answer = await prompts({
|
|
228
256
|
type: 'confirm',
|
|
229
257
|
name: 'confirmed',
|
|
230
|
-
message: '
|
|
258
|
+
message: '¿Permitir al agente ejecutar esta acción?',
|
|
231
259
|
initial: true,
|
|
232
260
|
});
|
|
233
261
|
|
|
234
262
|
const reqId = req.request_id || req.id || '';
|
|
235
263
|
if (answer && answer.confirmed) {
|
|
236
264
|
this.client.confirmAction(reqId);
|
|
237
|
-
console.log(GREEN('
|
|
265
|
+
console.log(GREEN(' ✔ Acción confirmada.\n'));
|
|
238
266
|
} else {
|
|
239
267
|
this.client.rejectAction(reqId);
|
|
240
|
-
console.log(RED('
|
|
268
|
+
console.log(RED(' ✖ Acción rechazada.\n'));
|
|
241
269
|
}
|
|
242
270
|
} finally {
|
|
243
271
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
@@ -269,7 +297,7 @@ export class HelixRepl {
|
|
|
269
297
|
});
|
|
270
298
|
|
|
271
299
|
rl.on('close', () => {
|
|
272
|
-
console.log(DIM('\nCerrando
|
|
300
|
+
console.log(DIM('\nCerrando sesión de HELIX CLI...'));
|
|
273
301
|
this.client.close();
|
|
274
302
|
process.exit(0);
|
|
275
303
|
});
|
|
@@ -283,7 +311,7 @@ export class HelixRepl {
|
|
|
283
311
|
this.activeTaskId = null;
|
|
284
312
|
this._promptUser();
|
|
285
313
|
} else {
|
|
286
|
-
console.log(DIM('\n
|
|
314
|
+
console.log(DIM('\n¡Hasta pronto!'));
|
|
287
315
|
this.client.close();
|
|
288
316
|
process.exit(0);
|
|
289
317
|
}
|
|
@@ -294,7 +322,7 @@ export class HelixRepl {
|
|
|
294
322
|
|
|
295
323
|
private _promptUser(): void {
|
|
296
324
|
if (!this.rl) return;
|
|
297
|
-
const promptPrefix = ` ${chalk.hex('#FF8C00').bold('helix')} ${chalk.gray('
|
|
325
|
+
const promptPrefix = ` ${chalk.hex('#FF8C00').bold('helix')} ${chalk.gray('â¯')} `;
|
|
298
326
|
this.rl.setPrompt(promptPrefix);
|
|
299
327
|
this.rl.prompt();
|
|
300
328
|
}
|
|
@@ -306,7 +334,7 @@ export class HelixRepl {
|
|
|
306
334
|
}
|
|
307
335
|
|
|
308
336
|
if (this.client.isStarting) {
|
|
309
|
-
console.log(`\n ${chalk.yellow('
|
|
337
|
+
console.log(`\n ${chalk.yellow('âš ï¸')} ${chalk.yellowBright('El servicio de HELIX se está iniciando en segundo plano. Espera a que termine antes de enviar comandos o reintentar.')}\n`);
|
|
310
338
|
this._promptUser();
|
|
311
339
|
return;
|
|
312
340
|
}
|
|
@@ -383,18 +411,48 @@ export class HelixRepl {
|
|
|
383
411
|
break;
|
|
384
412
|
}
|
|
385
413
|
|
|
386
|
-
case '/browser':
|
|
414
|
+
case '/browser': {
|
|
415
|
+
const browserArgs = args.trim().split(/\s+/);
|
|
416
|
+
if (browserArgs[0]?.toLowerCase() === 'live') {
|
|
417
|
+
const requestedState = browserArgs[1]?.toLowerCase();
|
|
418
|
+
if (requestedState && !['on', 'off'].includes(requestedState)) {
|
|
419
|
+
console.log(YELLOW(' Uso: /browser live [on|off]\n'));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
this.browserLiveViewEnabled = requestedState
|
|
423
|
+
? requestedState === 'on'
|
|
424
|
+
: !this.browserLiveViewEnabled;
|
|
425
|
+
console.log(
|
|
426
|
+
this.browserLiveViewEnabled
|
|
427
|
+
? GREEN(' Vista ASCII del navegador activada.\n')
|
|
428
|
+
: YELLOW(' Vista ASCII del navegador desactivada.\n'),
|
|
429
|
+
);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
387
432
|
await handleBrowserCommand(this.client, args);
|
|
388
433
|
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case '/subagents':
|
|
437
|
+
await handleSubagentsCommand(this.client, args);
|
|
438
|
+
break;
|
|
389
439
|
|
|
390
440
|
case '/tasks':
|
|
391
441
|
await this._handleListTasks();
|
|
392
442
|
break;
|
|
393
443
|
|
|
444
|
+
case '/model':
|
|
445
|
+
await handleModelCommand(this.client, args);
|
|
446
|
+
break;
|
|
447
|
+
|
|
394
448
|
case '/models':
|
|
395
449
|
await this._handleListModels();
|
|
396
450
|
break;
|
|
397
451
|
|
|
452
|
+
case '/memory':
|
|
453
|
+
await handleMemoryCommand(this.client, args);
|
|
454
|
+
break;
|
|
455
|
+
|
|
398
456
|
case '/status':
|
|
399
457
|
await this._handleStatus();
|
|
400
458
|
break;
|
|
@@ -416,7 +474,7 @@ export class HelixRepl {
|
|
|
416
474
|
this.client.emergencyStop();
|
|
417
475
|
this.isProcessingPrompt = false;
|
|
418
476
|
this.activeTaskId = null;
|
|
419
|
-
console.log(GREEN('
|
|
477
|
+
console.log(GREEN('✔ Todas las tareas han sido detenidas.\n'));
|
|
420
478
|
break;
|
|
421
479
|
|
|
422
480
|
case '/exit':
|
|
@@ -436,7 +494,7 @@ export class HelixRepl {
|
|
|
436
494
|
|
|
437
495
|
// --- Direct chat message / @skill / #mcp ---
|
|
438
496
|
if (!this.client.connected) {
|
|
439
|
-
console.log(YELLOW('
|
|
497
|
+
console.log(YELLOW('⚠El agente no está conectado actualmente. Intentando enviar de todos modos...'));
|
|
440
498
|
}
|
|
441
499
|
|
|
442
500
|
// Highlight skills in strong orange (#FF8C00) and mcps in cyan
|
|
@@ -448,8 +506,8 @@ export class HelixRepl {
|
|
|
448
506
|
this.isProcessingPrompt = true;
|
|
449
507
|
this.activeTasksCount++;
|
|
450
508
|
|
|
451
|
-
process.stdout.write(` ${chalk.gray('
|
|
452
|
-
process.stdout.write(` ${chalk.hex('#FF9F1C')('
|
|
509
|
+
process.stdout.write(` ${chalk.gray('→')} ${formattedEcho}\n`);
|
|
510
|
+
process.stdout.write(` ${chalk.hex('#FF9F1C')('â—†')} ${chalk.gray('Procesando...')}`);
|
|
453
511
|
|
|
454
512
|
const sent = this.client.sendUserText(
|
|
455
513
|
line,
|
|
@@ -459,7 +517,7 @@ export class HelixRepl {
|
|
|
459
517
|
|
|
460
518
|
if (!sent) {
|
|
461
519
|
process.stdout.write('\r\x1b[K');
|
|
462
|
-
console.log(RED('
|
|
520
|
+
console.log(RED('✖ Error: No se pudo enviar el mensaje (WebSocket desconectado).'));
|
|
463
521
|
this.isProcessingPrompt = false;
|
|
464
522
|
this.activeTasksCount = Math.max(0, this.activeTasksCount - 1);
|
|
465
523
|
this._promptUser();
|
|
@@ -469,7 +527,7 @@ export class HelixRepl {
|
|
|
469
527
|
private async _handleListTasks(): Promise<void> {
|
|
470
528
|
try {
|
|
471
529
|
const tasks = await this.client.getTasks();
|
|
472
|
-
console.log(`\n${BOLD('
|
|
530
|
+
console.log(`\n${BOLD('📋 Tareas Registradas en HELIX:')}\n`);
|
|
473
531
|
if (!tasks || tasks.length === 0) {
|
|
474
532
|
console.log(GRAY(' No hay tareas recientes registradas.\n'));
|
|
475
533
|
return;
|
|
@@ -477,10 +535,10 @@ export class HelixRepl {
|
|
|
477
535
|
|
|
478
536
|
for (const t of tasks.slice(-10)) {
|
|
479
537
|
let statusBadge = chalk.gray(t.status);
|
|
480
|
-
if (t.status === 'COMPLETED') statusBadge = chalk.green('
|
|
481
|
-
if (t.status === 'RUNNING') statusBadge = chalk.yellow('
|
|
482
|
-
if (t.status === 'FAILED') statusBadge = chalk.red('
|
|
483
|
-
if (t.status === 'CANCELLED') statusBadge = chalk.gray('
|
|
538
|
+
if (t.status === 'COMPLETED') statusBadge = chalk.green('✔ Completada');
|
|
539
|
+
if (t.status === 'RUNNING') statusBadge = chalk.yellow('ⳠEn ejecución');
|
|
540
|
+
if (t.status === 'FAILED') statusBadge = chalk.red('✖ Fallida');
|
|
541
|
+
if (t.status === 'CANCELLED') statusBadge = chalk.gray('â—‹ Cancelada');
|
|
484
542
|
|
|
485
543
|
console.log(` ${statusBadge} ${DIM(t.task_id)}`);
|
|
486
544
|
if (t.prompt) console.log(` ${chalk.white(t.prompt)}`);
|
|
@@ -494,14 +552,14 @@ export class HelixRepl {
|
|
|
494
552
|
private async _handleListModels(): Promise<void> {
|
|
495
553
|
try {
|
|
496
554
|
const models = await this.client.getModels();
|
|
497
|
-
console.log(`\n${BOLD('
|
|
555
|
+
console.log(`\n${BOLD('🧠Modelos Disponibles en Proveedores Configurados:')}\n`);
|
|
498
556
|
if (!models || models.length === 0) {
|
|
499
557
|
console.log(GRAY(' No hay modelos disponibles. Configura tus API Keys con /config-apikey.\n'));
|
|
500
558
|
return;
|
|
501
559
|
}
|
|
502
560
|
|
|
503
561
|
for (const m of models) {
|
|
504
|
-
console.log(` ${CYAN('
|
|
562
|
+
console.log(` ${CYAN('•')} ${chalk.bold(m.id)} ${DIM(`[${m.provider}]`)}`);
|
|
505
563
|
}
|
|
506
564
|
console.log('');
|
|
507
565
|
} catch (err) {
|
|
@@ -511,11 +569,11 @@ export class HelixRepl {
|
|
|
511
569
|
|
|
512
570
|
private async _handleStatus(): Promise<void> {
|
|
513
571
|
console.log('');
|
|
514
|
-
console.log(` ${chalk.hex('#FF8C00').bold('
|
|
515
|
-
console.log(` ${chalk.gray('
|
|
516
|
-
console.log(` ${chalk.gray('Agente WS')} ${this.client.connected ? chalk.green('
|
|
517
|
-
console.log(` ${chalk.gray('
|
|
518
|
-
console.log(` ${chalk.gray('
|
|
572
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ Estado del Sistema')}`);
|
|
573
|
+
console.log(` ${chalk.gray('─'.repeat(40))}`);
|
|
574
|
+
console.log(` ${chalk.gray('Agente WS')} ${this.client.connected ? chalk.green('â— Conectado') : chalk.red('â— Desconectado')} ${chalk.gray(this.client.url)}`);
|
|
575
|
+
console.log(` ${chalk.gray('Micrófono')} ${this.voiceEnabled ? chalk.green('◠Activo') + chalk.gray(' (hey helix)') : chalk.red('◠Inactivo')}`);
|
|
576
|
+
console.log(` ${chalk.gray('Conversación')} ${chalk.gray(this.conversationId)} ${chalk.gray(`(${this.conversationHistory.length} msgs)`)}`);
|
|
519
577
|
console.log(` ${chalk.gray('Skills')} ${chalk.hex('#FF8C00').bold(this.autocompleter.getSkills().length.toString())} registradas`);
|
|
520
578
|
console.log(` ${chalk.gray('MCP Tools')} ${chalk.cyan.bold(this.autocompleter.getMcpTools().length.toString())} disponibles`);
|
|
521
579
|
console.log('');
|
package/tests/cli.test.ts
CHANGED
|
@@ -7,6 +7,9 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { discoverUserSkillNames } from '../src/skills/user-skill-discovery';
|
|
10
|
+
import { renderSubagentPanel } from '../src/commands/subagents';
|
|
11
|
+
import { grayscaleToAscii, renderJpegAsAscii } from '../src/ui/browser-ascii';
|
|
12
|
+
import jpeg from 'jpeg-js';
|
|
10
13
|
|
|
11
14
|
test('Autocompleter suggests slash commands', () => {
|
|
12
15
|
const completer = new Autocompleter();
|
|
@@ -18,6 +21,7 @@ test('Autocompleter suggests slash commands', () => {
|
|
|
18
21
|
assert.ok(completions.includes('/mcp'));
|
|
19
22
|
assert.ok(completions.includes('/create-mcp'));
|
|
20
23
|
assert.ok(completions.includes('/browser'));
|
|
24
|
+
assert.ok(completions.includes('/subagents'));
|
|
21
25
|
assert.ok(completions.includes('/config-apikey'));
|
|
22
26
|
assert.ok(completions.includes('/show-apikey'));
|
|
23
27
|
assert.ok(completions.includes('/microphone'));
|
|
@@ -28,6 +32,43 @@ test('Autocompleter suggests slash commands', () => {
|
|
|
28
32
|
assert.ok(completions.includes('/clear'));
|
|
29
33
|
});
|
|
30
34
|
|
|
35
|
+
test('Subagent panel renders ASCII rows with current status and action', () => {
|
|
36
|
+
const panel = renderSubagentPanel(
|
|
37
|
+
[{ profile_id: 'flowbot', name: 'Flowbot', role: 'Browser automation' }],
|
|
38
|
+
[{
|
|
39
|
+
profile_id: 'flowbot',
|
|
40
|
+
status: 'running',
|
|
41
|
+
progress: 50,
|
|
42
|
+
current_action: 'Opening browser',
|
|
43
|
+
}],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
assert.ok(panel.includes('+'));
|
|
47
|
+
assert.ok(panel.includes('flowbot'));
|
|
48
|
+
assert.ok(panel.includes('Flowbot'));
|
|
49
|
+
assert.ok(panel.includes('RUNNING'));
|
|
50
|
+
assert.ok(panel.includes('50%'));
|
|
51
|
+
assert.ok(panel.includes('Opening browser'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Browser image renderer maps grayscale to a bounded ASCII frame', () => {
|
|
55
|
+
assert.equal(
|
|
56
|
+
grayscaleToAscii([[0, 255], [0, 255], [128, 64], [128, 64]], 2),
|
|
57
|
+
'@ \n=#',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Browser image renderer decodes JPEG frames and rejects invalid payloads', () => {
|
|
62
|
+
const image = jpeg.encode({
|
|
63
|
+
data: Buffer.from([255, 255, 255, 255]),
|
|
64
|
+
width: 1,
|
|
65
|
+
height: 1,
|
|
66
|
+
}, 80);
|
|
67
|
+
|
|
68
|
+
assert.equal(renderJpegAsAscii(image.data.toString('base64'), 1), ' ');
|
|
69
|
+
assert.throws(() => renderJpegAsAscii('not-a-jpeg'), /valid, bounded Base64 JPEG/);
|
|
70
|
+
});
|
|
71
|
+
|
|
31
72
|
test('Autocompleter filters slash commands by prefix', () => {
|
|
32
73
|
const completer = new Autocompleter();
|
|
33
74
|
|
package/tsconfig.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "commonjs",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"esModuleInterop": true,
|
|
10
10
|
"skipLibCheck": true,
|
|
11
11
|
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
// @ts-ignore - node is deprecated but required for CommonJS compatibility with current dependencies
|
|
13
12
|
"moduleResolution": "node",
|
|
14
13
|
"resolveJsonModule": true,
|
|
15
|
-
"declaration": true
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"types": ["node", "ws"]
|
|
16
16
|
},
|
|
17
17
|
"include": ["src/**/*"],
|
|
18
18
|
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|
|
19
|
+
}
|