helix-assistant-cli 0.0.0-stage → 0.2.1

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.
Files changed (51) hide show
  1. package/bin/helix.js +3 -0
  2. package/dist/client/agent-client.d.ts +60 -0
  3. package/dist/client/agent-client.js +322 -0
  4. package/dist/client/agent-launcher.d.ts +30 -0
  5. package/dist/client/agent-launcher.js +339 -0
  6. package/dist/client/types.d.ts +88 -0
  7. package/dist/client/types.js +2 -0
  8. package/dist/commands/apikey.d.ts +3 -0
  9. package/dist/commands/apikey.js +123 -0
  10. package/dist/commands/browser.d.ts +2 -0
  11. package/dist/commands/browser.js +35 -0
  12. package/dist/commands/mcp.d.ts +3 -0
  13. package/dist/commands/mcp.js +102 -0
  14. package/dist/commands/mic.d.ts +2 -0
  15. package/dist/commands/mic.js +57 -0
  16. package/dist/commands/skills.d.ts +3 -0
  17. package/dist/commands/skills.js +105 -0
  18. package/dist/config/env.d.ts +6 -0
  19. package/dist/config/env.js +71 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +126 -0
  22. package/dist/ui/autocomplete.d.ts +25 -0
  23. package/dist/ui/autocomplete.js +142 -0
  24. package/dist/ui/banner.d.ts +24 -0
  25. package/dist/ui/banner.js +207 -0
  26. package/dist/ui/chat.d.ts +1 -0
  27. package/dist/ui/chat.js +5 -0
  28. package/dist/ui/colors.d.ts +34 -0
  29. package/dist/ui/colors.js +69 -0
  30. package/dist/ui/repl.d.ts +31 -0
  31. package/dist/ui/repl.js +402 -0
  32. package/package.json +31 -4
  33. package/src/client/agent-client.ts +387 -0
  34. package/src/client/agent-launcher.ts +374 -0
  35. package/src/client/types.ts +95 -0
  36. package/src/commands/apikey.ts +129 -0
  37. package/src/commands/browser.ts +39 -0
  38. package/src/commands/mcp.ts +105 -0
  39. package/src/commands/mic.ts +57 -0
  40. package/src/commands/skills.ts +109 -0
  41. package/src/config/env.ts +63 -0
  42. package/src/index.ts +139 -0
  43. package/src/ui/autocomplete.ts +153 -0
  44. package/src/ui/banner.ts +240 -0
  45. package/src/ui/chat.ts +1 -0
  46. package/src/ui/colors.ts +70 -0
  47. package/src/ui/repl.ts +473 -0
  48. package/tests/cli.test.ts +141 -0
  49. package/tsconfig.json +18 -0
  50. package/tsconfig.test.json +7 -0
  51. package/README.md +0 -3
@@ -0,0 +1,105 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import type { AgentClient } from '../client/agent-client';
4
+ import type { McpServerConfig } from '../client/types';
5
+ import { formatMcp, CYAN_BOLD, GREEN, RED, YELLOW, GRAY, BOLD, DIM } from '../ui/colors';
6
+
7
+ export async function handleListMcp(client: AgentClient): Promise<void> {
8
+ console.log(`\n${BOLD('🔌 Servidores y Herramientas MCP:')}\n`);
9
+
10
+ try {
11
+ const servers = await client.getMcpServers();
12
+ if (!servers || servers.length === 0) {
13
+ console.log(GRAY(' No hay servidores MCP configurados actualmente.'));
14
+ console.log(DIM(' Usa /create-mcp para configurar uno nuevo.\n'));
15
+ return;
16
+ }
17
+
18
+ for (const server of servers) {
19
+ let statusBadge = chalk.gray('idle');
20
+ if (server.status === 'connected') {
21
+ statusBadge = chalk.green('● Conectado');
22
+ } else if (server.status === 'connecting') {
23
+ statusBadge = chalk.yellow('⏳ Conectando');
24
+ } else if (server.status === 'error') {
25
+ statusBadge = chalk.red('✖ Error');
26
+ }
27
+
28
+ console.log(` ${CYAN_BOLD('◈')} ${formatMcp(server.name)} [${statusBadge}]`);
29
+ console.log(` ${GRAY('Comando:')} ${chalk.white(server.command)}`);
30
+ if (server.error) {
31
+ console.log(` ${RED('Error:')} ${server.error}`);
32
+ }
33
+ if (server.tools && server.tools.length > 0) {
34
+ console.log(` ${DIM('Herramientas provistas:')}`);
35
+ for (const tool of server.tools) {
36
+ console.log(` ${DIM('•')} ${formatMcp(tool.name)}${tool.description ? DIM(` - ${tool.description}`) : ''}`);
37
+ }
38
+ }
39
+ console.log('');
40
+ }
41
+ } catch (err) {
42
+ console.log(RED(` Error al obtener servidores MCP: ${(err as Error).message}\n`));
43
+ }
44
+ }
45
+
46
+ export async function handleCreateMcp(client: AgentClient): Promise<void> {
47
+ console.log(`\n${BOLD('⚙️ Registrar Nuevo Servidor MCP')}\n`);
48
+
49
+ const answers = await prompts([
50
+ {
51
+ type: 'text',
52
+ name: 'name',
53
+ message: 'Nombre identificador del servidor MCP (ej. filesystem, github, fetch):',
54
+ validate: (v: string) =>
55
+ /^[a-zA-Z0-9_-]+$/.test(v) ? true : 'Usa solo letras, números, guiones y guiones bajos',
56
+ },
57
+ {
58
+ type: 'text',
59
+ name: 'command',
60
+ message: 'Comando ejecutable o npx (ej. npx -y @modelcontextprotocol/server-filesystem C:\\work):',
61
+ validate: (v: string) => (v && v.trim() ? true : 'El comando es requerido'),
62
+ },
63
+ {
64
+ type: 'text',
65
+ name: 'env',
66
+ message: 'Variables de entorno opcionales (formato KEY=VALUE,KEY2=VALUE2):',
67
+ initial: '',
68
+ },
69
+ ]);
70
+
71
+ if (!answers.name || !answers.command) {
72
+ console.log(YELLOW('\nOperación cancelada.\n'));
73
+ return;
74
+ }
75
+
76
+ const envObj: Record<string, string> = {};
77
+ if (answers.env && typeof answers.env === 'string') {
78
+ const pairs = answers.env.split(',');
79
+ for (const pair of pairs) {
80
+ const [k, v] = pair.split('=');
81
+ if (k && v) envObj[k.trim()] = v.trim();
82
+ }
83
+ }
84
+
85
+ try {
86
+ const currentSettings = await client.getSettings();
87
+ const existingServers = (currentSettings.mcp_servers as McpServerConfig[]) || [];
88
+
89
+ // Filter out existing with same name
90
+ const updatedServers = existingServers.filter((s) => s.name !== answers.name);
91
+ updatedServers.push({
92
+ name: answers.name.trim(),
93
+ command: answers.command.trim(),
94
+ env: Object.keys(envObj).length > 0 ? envObj : undefined,
95
+ });
96
+
97
+ console.log(GRAY(`\nGuardando servidor MCP ${formatMcp(answers.name)}...`));
98
+ await client.saveSettings({ mcp_servers: updatedServers });
99
+
100
+ console.log(GREEN(`✔ ¡Servidor MCP ${formatMcp(answers.name)} registrado con éxito!`));
101
+ console.log(DIM(` HELIX sincronizará las herramientas automáticamente.\n`));
102
+ } catch (err) {
103
+ console.log(RED(`✖ Error al registrar servidor MCP: ${(err as Error).message}\n`));
104
+ }
105
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import type { AgentClient } from '../client/agent-client';
3
+ import { GREEN, RED, YELLOW, GRAY, BOLD, DIM } from '../ui/colors';
4
+
5
+ export async function handleMicrophoneCommand(
6
+ client: AgentClient,
7
+ action?: string,
8
+ currentVoiceState: boolean = true
9
+ ): Promise<boolean> {
10
+ const normalizedAction = action?.toLowerCase().trim();
11
+
12
+ if (normalizedAction === 'enable' || normalizedAction === 'on' || normalizedAction === 'true') {
13
+ try {
14
+ console.log(GRAY('\nActivando micrófono en el agente HELIX...'));
15
+ await client.saveSettings({ voice_enabled: true });
16
+ console.log(`${GREEN('🟢 Micrófono:')} ${chalk.greenBright.bold('ACTIVO')} ${DIM('(Escuchando wake word: "hey helix").')}\n`);
17
+ return true;
18
+ } catch (err) {
19
+ console.log(RED(`✖ Error al activar micrófono: ${(err as Error).message}\n`));
20
+ return currentVoiceState;
21
+ }
22
+ }
23
+
24
+ if (normalizedAction === 'disable' || normalizedAction === 'off' || normalizedAction === 'false') {
25
+ try {
26
+ console.log(GRAY('\nDesactivando micrófono en el agente HELIX...'));
27
+ await client.saveSettings({ voice_enabled: false });
28
+ console.log(`${RED('🔴 Micrófono:')} ${chalk.redBright.bold('INACTIVO')} ${DIM('(Entrada por voz deshabilitada).')}\n`);
29
+ return false;
30
+ } catch (err) {
31
+ console.log(RED(`✖ Error al desactivar micrófono: ${(err as Error).message}\n`));
32
+ return currentVoiceState;
33
+ }
34
+ }
35
+
36
+ // If no action or invalid action, show current status and instructions
37
+ try {
38
+ const settings = await client.getSettings();
39
+ const isEnabled = settings.voice_enabled ?? currentVoiceState;
40
+ const wakeWord = (settings.wake_word as string) || 'hey helix';
41
+
42
+ console.log(`\n${BOLD('🎤 Estado del Micrófono y Reconocimiento de Voz:')}\n`);
43
+ if (isEnabled) {
44
+ console.log(` Estado: ${GREEN('🟢 ACTIVO')}`);
45
+ console.log(` Wake Word: ${chalk.cyan.bold(`"${wakeWord}"`)}`);
46
+ console.log(` Detalles: El agente procesará tu voz al decir "${wakeWord}".`);
47
+ } else {
48
+ console.log(` Estado: ${RED('🔴 INACTIVO')}`);
49
+ console.log(` Detalles: La captura de voz está desactivada.`);
50
+ }
51
+ console.log(`\n ${DIM('Uso: /microphone enable | /microphone disable')}\n`);
52
+ return isEnabled;
53
+ } catch (err) {
54
+ console.log(RED(` Error al consultar configuración de voz: ${(err as Error).message}\n`));
55
+ return currentVoiceState;
56
+ }
57
+ }
@@ -0,0 +1,109 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import type { AgentClient } from '../client/agent-client';
4
+ import { formatSkill, ORANGE_BOLD, GREEN, RED, YELLOW, GRAY, BOLD, DIM } from '../ui/colors';
5
+
6
+ export async function handleListSkills(client: AgentClient, specificSkill?: string): Promise<void> {
7
+ try {
8
+ const skills = await client.getSkills();
9
+ if (!skills || skills.length === 0) {
10
+ console.log(GRAY('\n No hay skills registradas en el agente.\n'));
11
+ return;
12
+ }
13
+
14
+ if (specificSkill) {
15
+ const match = skills.find(
16
+ (s) => s.name.toLowerCase() === specificSkill.toLowerCase() || `@${s.name.toLowerCase()}` === specificSkill.toLowerCase()
17
+ );
18
+ if (match) {
19
+ console.log(`\n${BOLD('Detalles de la Skill:')}`);
20
+ console.log(` Nombre: ${formatSkill(match.name)}`);
21
+ console.log(` Descripción: ${chalk.white(match.description || 'Sin descripción')}`);
22
+ if (match.triggers && match.triggers.length > 0) {
23
+ console.log(` Disparadores: ${DIM(match.triggers.join(', '))}`);
24
+ }
25
+ if (match.instructions) {
26
+ console.log(` Instrucciones:\n${GRAY(' ' + match.instructions.replace(/\n/g, '\n '))}`);
27
+ }
28
+ console.log('');
29
+ return;
30
+ }
31
+ console.log(YELLOW(`\n Skill "${specificSkill}" no encontrada. Listando todas las disponibles:\n`));
32
+ }
33
+
34
+ console.log(`\n${BOLD('⚡ Skills Registradas en HELIX:')}\n`);
35
+ for (const skill of skills) {
36
+ const nameTag = formatSkill(skill.name);
37
+ const isCustom = skill.custom ? chalk.hex('#FFA500')(' [custom]') : '';
38
+ console.log(` ${ORANGE_BOLD('✦')} ${nameTag}${isCustom}`);
39
+ if (skill.description) {
40
+ console.log(` ${DIM(skill.description)}`);
41
+ }
42
+ if (skill.triggers && skill.triggers.length > 0) {
43
+ console.log(` ${GRAY('Triggers:')} ${DIM(skill.triggers.slice(0, 4).join(', '))}`);
44
+ }
45
+ console.log('');
46
+ }
47
+ } catch (err) {
48
+ console.log(RED(` Error al obtener skills: ${(err as Error).message}\n`));
49
+ }
50
+ }
51
+
52
+ export async function handleCreateSkill(client: AgentClient): Promise<void> {
53
+ console.log(`\n${BOLD('🛠️ Crear Nueva Skill en HELIX')}\n`);
54
+
55
+ const answers = await prompts([
56
+ {
57
+ type: 'text',
58
+ name: 'name',
59
+ message: 'Nombre de la skill (ej. web-summarizer, code-auditor):',
60
+ validate: (v: string) =>
61
+ /^[a-zA-Z0-9_-]+$/.test(v)
62
+ ? true
63
+ : 'El nombre solo puede contener letras, números, guiones y guiones bajos',
64
+ },
65
+ {
66
+ type: 'text',
67
+ name: 'description',
68
+ message: 'Descripción corta de la funcionalidad:',
69
+ validate: (v: string) => (v && v.trim() ? true : 'La descripción es requerida'),
70
+ },
71
+ {
72
+ type: 'text',
73
+ name: 'triggers',
74
+ message: 'Palabras clave disparadoras (separadas por coma, opcional):',
75
+ initial: '',
76
+ },
77
+ {
78
+ type: 'text',
79
+ name: 'instructions',
80
+ message: 'Instrucciones o system prompt para la ejecución de la skill:',
81
+ validate: (v: string) => (v && v.trim() ? true : 'Las instrucciones son requeridas'),
82
+ },
83
+ ]);
84
+
85
+ if (!answers.name || !answers.description || !answers.instructions) {
86
+ console.log(YELLOW('\nOperación cancelada.\n'));
87
+ return;
88
+ }
89
+
90
+ const triggersList = typeof answers.triggers === 'string'
91
+ ? answers.triggers.split(',').map((t: string) => t.trim()).filter(Boolean)
92
+ : [];
93
+
94
+ const skillPayload = {
95
+ name: answers.name.trim(),
96
+ description: answers.description.trim(),
97
+ triggers: triggersList,
98
+ instructions: answers.instructions.trim(),
99
+ };
100
+
101
+ try {
102
+ console.log(GRAY(`\nGuardando skill ${formatSkill(skillPayload.name)}...`));
103
+ await client.saveSkill(skillPayload);
104
+ console.log(GREEN(`✔ ¡Skill ${formatSkill(skillPayload.name)} creada con éxito!`));
105
+ console.log(DIM(` Puedes invocarla usando: ${formatSkill(skillPayload.name)} <tu consulta>\n`));
106
+ } catch (err) {
107
+ console.log(RED(`✖ Error al crear la skill: ${(err as Error).message}\n`));
108
+ }
109
+ }
@@ -0,0 +1,63 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import dotenv from 'dotenv';
4
+
5
+ // Discover .env from current directory, parent dirs, or monorepo root
6
+ export function loadHelixEnv(): void {
7
+ const searchDirs = [
8
+ process.cwd(),
9
+ path.resolve(process.cwd(), '../..'),
10
+ path.resolve(__dirname, '../../..'),
11
+ path.resolve(__dirname, '../../../..'),
12
+ ];
13
+
14
+ for (const dir of searchDirs) {
15
+ const envPath = path.join(dir, '.env');
16
+ if (fs.existsSync(envPath)) {
17
+ dotenv.config({ path: envPath });
18
+ break;
19
+ }
20
+ }
21
+ }
22
+
23
+ // Automatically load on import
24
+ loadHelixEnv();
25
+
26
+ export function getWsPort(): number {
27
+ if (process.env.AGENT_WS_PORT && !isNaN(Number(process.env.AGENT_WS_PORT))) {
28
+ return Number(process.env.AGENT_WS_PORT);
29
+ }
30
+ return 8765;
31
+ }
32
+
33
+ export function getWsUrl(override?: string): string {
34
+ if (override) return override;
35
+ if (process.env.AGENT_WS_URL) return process.env.AGENT_WS_URL;
36
+ return `ws://127.0.0.1:${getWsPort()}`;
37
+ }
38
+
39
+ export function getTokenFilePath(): string {
40
+ const appData = process.env.APPDATA || (process.platform === 'darwin' ? path.join(process.env.HOME || '', 'Library/Application Support') : path.join(process.env.HOME || '', '.config'));
41
+ return path.join(appData, 'HELIX', '.ws_token');
42
+ }
43
+
44
+ export function getWsToken(override?: string): string | undefined {
45
+ if (override) return override;
46
+ if (process.env.HELIX_WS_TOKEN) return process.env.HELIX_WS_TOKEN;
47
+ const tokenFile = getTokenFilePath();
48
+ if (fs.existsSync(tokenFile)) {
49
+ try {
50
+ const saved = fs.readFileSync(tokenFile, 'utf-8').trim();
51
+ if (saved.length >= 16) {
52
+ process.env.HELIX_WS_TOKEN = saved;
53
+ return saved;
54
+ }
55
+ } catch {}
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ export function getSettingsFilePath(): string {
61
+ const appData = process.env.APPDATA || (process.platform === 'darwin' ? path.join(process.env.HOME || '', 'Library/Application Support') : path.join(process.env.HOME || '', '.config'));
62
+ return path.join(appData, 'HELIX', 'settings.json');
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { Command } from 'commander';
2
+ import { HelixRepl } from './ui/repl';
3
+ import { getWsUrl, getWsToken } from './config/env';
4
+ import { AgentClient } from './client/agent-client';
5
+ import type { HelixSettings } from './client/types';
6
+ import { handleShowApiKey } from './commands/apikey';
7
+ import { ORANGE, CYAN, GREEN, RED, BOLD, DIM } from './ui/colors';
8
+ import { getAsciiLogo } from './ui/banner';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('helix')
14
+ .description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
15
+ .version('0.1.0')
16
+ .option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', getWsUrl())
17
+ .option('-t, --token <token>', 'Token de autenticación WebSocket de HELIX', getWsToken())
18
+ .action(async (options) => {
19
+ // Default action: start interactive chat REPL
20
+ const repl = new HelixRepl({
21
+ wsUrl: options.wsUrl || getWsUrl(),
22
+ wsToken: options.token || getWsToken(),
23
+ });
24
+ await repl.start();
25
+ });
26
+
27
+ program
28
+ .command('chat')
29
+ .description('Iniciar sesión de chat interactivo con HELIX')
30
+ .action(async () => {
31
+ const opts = program.opts();
32
+ const repl = new HelixRepl({
33
+ wsUrl: opts.wsUrl || getWsUrl(),
34
+ wsToken: opts.token || getWsToken(),
35
+ });
36
+ await repl.start();
37
+ });
38
+
39
+ program
40
+ .command('run <prompt>')
41
+ .description('Ejecutar un prompt directamente y esperar la respuesta')
42
+ .action(async (promptText: string) => {
43
+ const opts = program.opts();
44
+ const client = new AgentClient({
45
+ wsUrl: opts.wsUrl || getWsUrl(),
46
+ wsToken: opts.token || getWsToken(),
47
+ autoReconnect: false,
48
+ });
49
+
50
+ console.log(CYAN(`Conectando con HELIX en ${client.url}...`));
51
+ try {
52
+ await client.connect();
53
+ } catch (err) {
54
+ console.error(RED(`No se pudo conectar al agente HELIX: ${(err as Error).message}`));
55
+ process.exit(1);
56
+ }
57
+
58
+ console.log(DIM(`Prompt: ${promptText}\n`));
59
+ const convId = `cli_run_${Date.now()}`;
60
+
61
+ client.on('react_step', (step: { iteration?: number }) => {
62
+ process.stdout.write(`\r${DIM(`[ReAct Step ${step?.iteration || 1}...]`)}`);
63
+ });
64
+
65
+ client.on('agent_message', (payload: { content?: string }) => {
66
+ process.stdout.write('\r\x1b[K');
67
+ console.log(payload?.content || '');
68
+ client.close();
69
+ process.exit(0);
70
+ });
71
+
72
+ client.sendUserText(promptText, convId);
73
+
74
+ // Timeout safety
75
+ setTimeout(() => {
76
+ console.error(RED('\nTiempo de espera agotado sin respuesta del agente.'));
77
+ client.close();
78
+ process.exit(1);
79
+ }, 60000);
80
+ });
81
+
82
+ program
83
+ .command('status')
84
+ .description('Consultar el estado del agente y los subsistemas')
85
+ .action(async () => {
86
+ const opts = program.opts();
87
+ const client = new AgentClient({
88
+ wsUrl: opts.wsUrl || getWsUrl(),
89
+ wsToken: opts.token || getWsToken(),
90
+ autoReconnect: false,
91
+ });
92
+
93
+ console.log(getAsciiLogo());
94
+ console.log(BOLD('Consultando estado de HELIX...\n'));
95
+
96
+ try {
97
+ await client.connect();
98
+ console.log(` ${GREEN('● Conectado')} al agente en ${client.url}`);
99
+
100
+ const [settings, skills, mcpServers] = await Promise.all([
101
+ client.getSettings().catch(() => ({} as HelixSettings)),
102
+ client.getSkills().catch(() => []),
103
+ client.getMcpServers().catch(() => []),
104
+ ]);
105
+
106
+ const micStatus = settings.voice_enabled !== false ? GREEN('🟢 Activo') : RED('🔴 Inactivo');
107
+ console.log(` Micrófono: ${micStatus}`);
108
+ console.log(` Skills: ${ORANGE(skills.length.toString())} registradas`);
109
+ console.log(` MCP Servers: ${CYAN(mcpServers.length.toString())} configurados`);
110
+ console.log('');
111
+ client.close();
112
+ } catch (err) {
113
+ console.log(` ${RED('● Desconectado')} del agente en ${client.url}`);
114
+ console.log(DIM(` Detalle: ${(err as Error).message}\n`));
115
+ }
116
+ });
117
+
118
+ program
119
+ .command('apikeys')
120
+ .description('Mostrar el estado de las claves API configuradas')
121
+ .action(async () => {
122
+ const opts = program.opts();
123
+ const client = new AgentClient({
124
+ wsUrl: opts.wsUrl || getWsUrl(),
125
+ wsToken: opts.token || getWsToken(),
126
+ autoReconnect: false,
127
+ });
128
+
129
+ try {
130
+ await client.connect();
131
+ await handleShowApiKey(client);
132
+ } catch (err) {
133
+ console.error(RED(`Error de conexión con el agente: ${(err as Error).message}`));
134
+ } finally {
135
+ client.close();
136
+ }
137
+ });
138
+
139
+ program.parse(process.argv);
@@ -0,0 +1,153 @@
1
+ import chalk from 'chalk';
2
+ import type { AgentClient } from '../client/agent-client';
3
+ import { formatSkill, formatMcp, ORANGE, CYAN, MAGENTA, DIM } from './colors';
4
+
5
+ export interface CommandDefinition {
6
+ command: string;
7
+ description: string;
8
+ usage?: string;
9
+ }
10
+
11
+ export const SLASH_COMMANDS: CommandDefinition[] = [
12
+ { command: '/create-skill', description: 'Crear una nueva skill interactiva para HELIX' },
13
+ { command: '/skill', description: 'Listar o inspeccionar skills registradas', usage: '/skill [nombre]' },
14
+ { command: '/mcp', description: 'Listar servidores MCP y sus herramientas' },
15
+ { command: '/create-mcp', description: 'Registrar y configurar un nuevo servidor MCP' },
16
+ { command: '/browser', description: 'Ver estado del navegador web o abrir URLs', usage: '/browser [open <url>]' },
17
+ { command: '/config-apikey', description: 'Configurador interactivo de API Keys (OpenAI, Anthropic, Gemini, etc.)' },
18
+ { command: '/show-apikey', description: 'Ver estado y validez de las API Keys configuradas' },
19
+ { command: '/microphone', description: 'Ver o alternar estado del micrófono', usage: '/microphone [enable|disable]' },
20
+ { command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
21
+ { command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
22
+ { command: '/status', description: 'Mostrar resumen del estado del sistema' },
23
+ { command: '/clear', description: 'Limpiar pantalla y restaurar el banner de HELIX' },
24
+ { command: '/stop', description: 'Parada de emergencia de todas las tareas activas' },
25
+ { command: '/help', description: 'Mostrar ayuda de comandos y atajos' },
26
+ { command: '/exit', description: 'Salir del CLI' },
27
+ ];
28
+
29
+ export class Autocompleter {
30
+ private skills: string[] = [];
31
+ private mcpTools: string[] = [];
32
+
33
+ constructor(private client?: AgentClient) {}
34
+
35
+ public async refreshSuggestions(): Promise<void> {
36
+ if (!this.client || !this.client.connected) return;
37
+ try {
38
+ const skills = await this.client.getSkills();
39
+ if (skills) {
40
+ this.skills = skills.map((s) => s.name);
41
+ }
42
+ } catch {
43
+ // Ignored if client is offline
44
+ }
45
+
46
+ try {
47
+ const servers = await this.client.getMcpServers();
48
+ if (servers) {
49
+ const tools: string[] = [];
50
+ for (const s of servers) {
51
+ tools.push(s.name);
52
+ if (s.tools) {
53
+ for (const t of s.tools) {
54
+ tools.push(`${s.name}_${t.name}`);
55
+ }
56
+ }
57
+ }
58
+ this.mcpTools = tools;
59
+ }
60
+ } catch {
61
+ // Ignored
62
+ }
63
+ }
64
+
65
+ public getSkills(): string[] {
66
+ return this.skills;
67
+ }
68
+
69
+ public getMcpTools(): string[] {
70
+ return this.mcpTools;
71
+ }
72
+
73
+ /**
74
+ * Completer function matching readline specification:
75
+ * (line: string) => [string[], string]
76
+ */
77
+ public completer(line: string): [string[], string] {
78
+ const trimmed = line.trimStart();
79
+
80
+ // 1. Skill sub-arguments for /skill <name>
81
+ if (trimmed.startsWith('/skill ')) {
82
+ const arg = trimmed.slice('/skill '.length).toLowerCase();
83
+ const hits = this.skills
84
+ .filter((s) => s.toLowerCase().startsWith(arg))
85
+ .map((s) => `/skill ${s}`);
86
+ return [hits.length ? hits : this.skills.map((s) => `/skill ${s}`), line];
87
+ }
88
+
89
+ // 2. Microphone sub-arguments
90
+ if (trimmed.startsWith('/microphone ')) {
91
+ const arg = trimmed.slice('/microphone '.length).toLowerCase();
92
+ const options = ['/microphone enable', '/microphone disable'];
93
+ const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
94
+ return [hits.length ? hits : options, line];
95
+ }
96
+
97
+ // 3. Browser sub-arguments
98
+ if (trimmed.startsWith('/browser ')) {
99
+ const options = ['/browser open https://'];
100
+ return [options, line];
101
+ }
102
+
103
+ // 4. Slash command autocompletion
104
+ if (trimmed.startsWith('/')) {
105
+ const hits = SLASH_COMMANDS
106
+ .map((c) => c.command)
107
+ .filter((c) => c.toLowerCase().startsWith(trimmed.toLowerCase()));
108
+ return [hits.length ? hits : SLASH_COMMANDS.map((c) => c.command), line];
109
+ }
110
+
111
+ // 5. In-line @skill mention autocompletion
112
+ const lastAtMatch = /(?:^|\s)(@)([a-zA-Z0-9_-]*)$/.exec(line);
113
+ if (lastAtMatch) {
114
+ const prefix = line.slice(0, line.length - lastAtMatch[2].length);
115
+ const query = lastAtMatch[2].toLowerCase();
116
+ const matches = this.skills.filter((s) => s.toLowerCase().startsWith(query));
117
+ const hits = (matches.length ? matches : this.skills).map((s) => `${prefix}${s} `);
118
+ return [hits, line];
119
+ }
120
+
121
+ // 6. In-line #mcp mention autocompletion
122
+ const lastHashMatch = /(?:^|\s)(#)([a-zA-Z0-9_-]*)$/.exec(line);
123
+ if (lastHashMatch) {
124
+ const prefix = line.slice(0, line.length - lastHashMatch[2].length);
125
+ const query = lastHashMatch[2].toLowerCase();
126
+ const matches = this.mcpTools.filter((t) => t.toLowerCase().startsWith(query));
127
+ const hits = (matches.length ? matches : this.mcpTools).map((t) => `${prefix}${t} `);
128
+ return [hits, line];
129
+ }
130
+
131
+ return [[], line];
132
+ }
133
+
134
+ /**
135
+ * Formats the list of commands for display.
136
+ */
137
+ public renderHelp(): void {
138
+ console.log('');
139
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ Comandos Disponibles')} ${chalk.gray('(/ + [Tab] para autocompletar)')}`);
140
+ console.log(` ${chalk.gray('─'.repeat(50))}`);
141
+ for (const cmd of SLASH_COMMANDS) {
142
+ const usage = cmd.usage ? ` ${chalk.gray(cmd.usage)}` : '';
143
+ console.log(` ${chalk.magenta(cmd.command.padEnd(16))} ${chalk.gray(cmd.description)}${usage}`);
144
+ }
145
+
146
+ console.log('');
147
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ Prefijos Especiales')}`);
148
+ console.log(` ${chalk.gray('─'.repeat(50))}`);
149
+ console.log(` ${formatSkill('nombre')} ${chalk.gray('Invoca una skill registrada')}`);
150
+ console.log(` ${formatMcp('herramienta')} ${chalk.gray('Invoca herramientas MCP')}`);
151
+ console.log('');
152
+ }
153
+ }