helix-assistant-cli 0.0.0-stage → 0.2.4
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/bin/helix.js +3 -0
- package/dist/client/agent-client.d.ts +60 -0
- package/dist/client/agent-client.js +322 -0
- package/dist/client/agent-launcher.d.ts +30 -0
- package/dist/client/agent-launcher.js +339 -0
- package/dist/client/types.d.ts +88 -0
- package/dist/client/types.js +2 -0
- package/dist/commands/apikey.d.ts +3 -0
- package/dist/commands/apikey.js +123 -0
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +102 -0
- package/dist/commands/mic.d.ts +2 -0
- package/dist/commands/mic.js +57 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +105 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +193 -0
- package/dist/ui/autocomplete.d.ts +25 -0
- package/dist/ui/autocomplete.js +142 -0
- package/dist/ui/banner.d.ts +15 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/chat.d.ts +1 -0
- package/dist/ui/chat.js +5 -0
- package/dist/ui/colors.d.ts +34 -0
- package/dist/ui/colors.js +69 -0
- package/dist/ui/repl.d.ts +31 -0
- package/dist/ui/repl.js +448 -0
- package/package.json +30 -3
- package/src/client/agent-client.ts +387 -0
- package/src/client/agent-launcher.ts +374 -0
- package/src/client/types.ts +95 -0
- package/src/commands/apikey.ts +129 -0
- package/src/commands/browser.ts +39 -0
- package/src/commands/mcp.ts +105 -0
- package/src/commands/mic.ts +57 -0
- package/src/commands/skills.ts +109 -0
- package/src/config/env.ts +63 -0
- package/src/index.ts +208 -0
- package/src/ui/autocomplete.ts +153 -0
- package/src/ui/banner.ts +85 -0
- package/src/ui/chat.ts +1 -0
- package/src/ui/colors.ts +70 -0
- package/src/ui/repl.ts +504 -0
- package/tests/cli.test.ts +187 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +7 -0
- 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,208 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// Cross-Platform Terminal & Stream Encoding Setup
|
|
3
|
+
// Compatible with: Windows CMD (CP850/437/1252/65001), PowerShell,
|
|
4
|
+
// Linux terminal (xterm/kitty/gnome), and Bash (MSYS2/Git Bash/WSL).
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
|
+
if (process.stdin.setEncoding) {
|
|
7
|
+
process.stdin.setEncoding('utf-8');
|
|
8
|
+
}
|
|
9
|
+
if (process.stdout.setDefaultEncoding) {
|
|
10
|
+
process.stdout.setDefaultEncoding('utf-8');
|
|
11
|
+
}
|
|
12
|
+
if (process.stderr.setDefaultEncoding) {
|
|
13
|
+
process.stderr.setDefaultEncoding('utf-8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// On Windows, force console code page to 65001 (UTF-8)
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
try {
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
execSync('chcp 65001 >nul 2>&1', { stdio: 'ignore' });
|
|
21
|
+
} catch {
|
|
22
|
+
// Silently continue if chcp is unavailable
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import { Command } from 'commander';
|
|
28
|
+
import { HelixRepl } from './ui/repl';
|
|
29
|
+
import { getWsUrl, getWsToken } from './config/env';
|
|
30
|
+
import { AgentClient } from './client/agent-client';
|
|
31
|
+
import type { HelixSettings } from './client/types';
|
|
32
|
+
import { handleShowApiKey } from './commands/apikey';
|
|
33
|
+
import { ORANGE, CYAN, GREEN, RED, BOLD, DIM } from './ui/colors';
|
|
34
|
+
import { getAsciiLogo } from './ui/banner';
|
|
35
|
+
|
|
36
|
+
async function readStdin(): Promise<string> {
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let input = '';
|
|
42
|
+
const onData = (chunk: Buffer | string) => {
|
|
43
|
+
input += chunk.toString();
|
|
44
|
+
};
|
|
45
|
+
process.stdin.on('data', onData);
|
|
46
|
+
process.stdin.once('end', () => {
|
|
47
|
+
cleanup();
|
|
48
|
+
if (input.charCodeAt(0) === 0xfeff) input = input.slice(1);
|
|
49
|
+
resolve(input.trim());
|
|
50
|
+
});
|
|
51
|
+
process.stdin.once('error', () => {
|
|
52
|
+
cleanup();
|
|
53
|
+
resolve('');
|
|
54
|
+
});
|
|
55
|
+
// If no data arrives and stream is not ended within 300ms, abort to prevent hanging
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
cleanup();
|
|
58
|
+
if (input.charCodeAt(0) === 0xfeff) input = input.slice(1);
|
|
59
|
+
resolve(input.trim());
|
|
60
|
+
}, 300);
|
|
61
|
+
|
|
62
|
+
function cleanup() {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
process.stdin.removeListener('data', onData);
|
|
65
|
+
process.stdin.pause();
|
|
66
|
+
}
|
|
67
|
+
process.stdin.resume();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const program = new Command();
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.name('helix')
|
|
75
|
+
.description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
|
|
76
|
+
.version('0.1.0')
|
|
77
|
+
.option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', getWsUrl())
|
|
78
|
+
.option('-t, --token <token>', 'Token de autenticación WebSocket de HELIX', getWsToken())
|
|
79
|
+
.action(async (options) => {
|
|
80
|
+
// Default action: start interactive chat REPL
|
|
81
|
+
const repl = new HelixRepl({
|
|
82
|
+
wsUrl: options.wsUrl || getWsUrl(),
|
|
83
|
+
wsToken: options.token || getWsToken(),
|
|
84
|
+
});
|
|
85
|
+
await repl.start();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command('chat')
|
|
90
|
+
.description('Iniciar sesión de chat interactivo con HELIX')
|
|
91
|
+
.action(async () => {
|
|
92
|
+
const opts = program.opts();
|
|
93
|
+
const repl = new HelixRepl({
|
|
94
|
+
wsUrl: opts.wsUrl || getWsUrl(),
|
|
95
|
+
wsToken: opts.token || getWsToken(),
|
|
96
|
+
});
|
|
97
|
+
await repl.start();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
program
|
|
101
|
+
.command('run [prompt...]')
|
|
102
|
+
.description('Ejecutar un prompt directamente y esperar la respuesta (soporta pipes: cat file | helix run)')
|
|
103
|
+
.action(async (promptParts: string[]) => {
|
|
104
|
+
let promptText = (promptParts || []).join(' ').trim();
|
|
105
|
+
if (!promptText || promptText === '-') {
|
|
106
|
+
promptText = await readStdin();
|
|
107
|
+
}
|
|
108
|
+
if (!promptText) {
|
|
109
|
+
console.error(RED('Error: Debes proporcionar un prompt como argumento o a través de stdin (ej: echo "hola" | helix run).'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const opts = program.opts();
|
|
113
|
+
const client = new AgentClient({
|
|
114
|
+
wsUrl: opts.wsUrl || getWsUrl(),
|
|
115
|
+
wsToken: opts.token || getWsToken(),
|
|
116
|
+
autoReconnect: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(DIM(`Prompt: ${promptText}\n`));
|
|
120
|
+
console.log(CYAN(`Conectando con HELIX en ${client.url}...`));
|
|
121
|
+
try {
|
|
122
|
+
await client.connect();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(RED(`No se pudo conectar al agente HELIX: ${(err as Error).message}`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const convId = `cli_run_${Date.now()}`;
|
|
129
|
+
|
|
130
|
+
client.on('react_step', (step: { iteration?: number }) => {
|
|
131
|
+
process.stdout.write(`\r${DIM(`[ReAct Step ${step?.iteration || 1}...]`)}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
client.on('agent_message', (payload: { content?: string }) => {
|
|
135
|
+
process.stdout.write('\r\x1b[K');
|
|
136
|
+
console.log(payload?.content || '');
|
|
137
|
+
client.close();
|
|
138
|
+
process.exit(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
client.sendUserText(promptText, convId);
|
|
142
|
+
|
|
143
|
+
// Timeout safety
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
console.error(RED('\nTiempo de espera agotado sin respuesta del agente.'));
|
|
146
|
+
client.close();
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}, 60000);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command('status')
|
|
153
|
+
.description('Consultar el estado del agente y los subsistemas')
|
|
154
|
+
.action(async () => {
|
|
155
|
+
const opts = program.opts();
|
|
156
|
+
const client = new AgentClient({
|
|
157
|
+
wsUrl: opts.wsUrl || getWsUrl(),
|
|
158
|
+
wsToken: opts.token || getWsToken(),
|
|
159
|
+
autoReconnect: false,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(getAsciiLogo());
|
|
163
|
+
console.log(BOLD('Consultando estado de HELIX...\n'));
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await client.connect();
|
|
167
|
+
console.log(` ${GREEN('● Conectado')} al agente en ${client.url}`);
|
|
168
|
+
|
|
169
|
+
const [settings, skills, mcpServers] = await Promise.all([
|
|
170
|
+
client.getSettings().catch(() => ({} as HelixSettings)),
|
|
171
|
+
client.getSkills().catch(() => []),
|
|
172
|
+
client.getMcpServers().catch(() => []),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const micStatus = settings.voice_enabled !== false ? GREEN('🟢 Activo') : RED('🔴 Inactivo');
|
|
176
|
+
console.log(` Micrófono: ${micStatus}`);
|
|
177
|
+
console.log(` Skills: ${ORANGE(skills.length.toString())} registradas`);
|
|
178
|
+
console.log(` MCP Servers: ${CYAN(mcpServers.length.toString())} configurados`);
|
|
179
|
+
console.log('');
|
|
180
|
+
client.close();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.log(` ${RED('● Desconectado')} del agente en ${client.url}`);
|
|
183
|
+
console.log(DIM(` Detalle: ${(err as Error).message}\n`));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
program
|
|
188
|
+
.command('apikeys')
|
|
189
|
+
.description('Mostrar el estado de las claves API configuradas')
|
|
190
|
+
.action(async () => {
|
|
191
|
+
const opts = program.opts();
|
|
192
|
+
const client = new AgentClient({
|
|
193
|
+
wsUrl: opts.wsUrl || getWsUrl(),
|
|
194
|
+
wsToken: opts.token || getWsToken(),
|
|
195
|
+
autoReconnect: false,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await client.connect();
|
|
200
|
+
await handleShowApiKey(client);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(RED(`Error de conexión con el agente: ${(err as Error).message}`));
|
|
203
|
+
} finally {
|
|
204
|
+
client.close();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
program.parse(process.argv);
|