helix-assistant-cli 0.2.1 → 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/dist/index.js +71 -4
- package/dist/ui/banner.d.ts +3 -12
- package/dist/ui/banner.js +22 -145
- package/dist/ui/repl.js +71 -25
- package/package.json +2 -2
- package/src/index.ts +73 -4
- package/src/ui/banner.ts +22 -177
- package/src/ui/repl.ts +58 -27
- package/tests/cli.test.ts +46 -0
- package/tsconfig.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
// Cross-Platform Terminal & Stream Encoding Setup
|
|
5
|
+
// Compatible with: Windows CMD (CP850/437/1252/65001), PowerShell,
|
|
6
|
+
// Linux terminal (xterm/kitty/gnome), and Bash (MSYS2/Git Bash/WSL).
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
if (process.stdin.setEncoding) {
|
|
9
|
+
process.stdin.setEncoding('utf-8');
|
|
10
|
+
}
|
|
11
|
+
if (process.stdout.setDefaultEncoding) {
|
|
12
|
+
process.stdout.setDefaultEncoding('utf-8');
|
|
13
|
+
}
|
|
14
|
+
if (process.stderr.setDefaultEncoding) {
|
|
15
|
+
process.stderr.setDefaultEncoding('utf-8');
|
|
16
|
+
}
|
|
17
|
+
// On Windows, force console code page to 65001 (UTF-8)
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
try {
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
execSync('chcp 65001 >nul 2>&1', { stdio: 'ignore' });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Silently continue if chcp is unavailable
|
|
25
|
+
}
|
|
26
|
+
}
|
|
3
27
|
const commander_1 = require("commander");
|
|
4
28
|
const repl_1 = require("./ui/repl");
|
|
5
29
|
const env_1 = require("./config/env");
|
|
@@ -7,6 +31,41 @@ const agent_client_1 = require("./client/agent-client");
|
|
|
7
31
|
const apikey_1 = require("./commands/apikey");
|
|
8
32
|
const colors_1 = require("./ui/colors");
|
|
9
33
|
const banner_1 = require("./ui/banner");
|
|
34
|
+
async function readStdin() {
|
|
35
|
+
if (process.stdin.isTTY) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let input = '';
|
|
40
|
+
const onData = (chunk) => {
|
|
41
|
+
input += chunk.toString();
|
|
42
|
+
};
|
|
43
|
+
process.stdin.on('data', onData);
|
|
44
|
+
process.stdin.once('end', () => {
|
|
45
|
+
cleanup();
|
|
46
|
+
if (input.charCodeAt(0) === 0xfeff)
|
|
47
|
+
input = input.slice(1);
|
|
48
|
+
resolve(input.trim());
|
|
49
|
+
});
|
|
50
|
+
process.stdin.once('error', () => {
|
|
51
|
+
cleanup();
|
|
52
|
+
resolve('');
|
|
53
|
+
});
|
|
54
|
+
// If no data arrives and stream is not ended within 300ms, abort to prevent hanging
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
cleanup();
|
|
57
|
+
if (input.charCodeAt(0) === 0xfeff)
|
|
58
|
+
input = input.slice(1);
|
|
59
|
+
resolve(input.trim());
|
|
60
|
+
}, 300);
|
|
61
|
+
function cleanup() {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
process.stdin.removeListener('data', onData);
|
|
64
|
+
process.stdin.pause();
|
|
65
|
+
}
|
|
66
|
+
process.stdin.resume();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
10
69
|
const program = new commander_1.Command();
|
|
11
70
|
program
|
|
12
71
|
.name('helix')
|
|
@@ -34,15 +93,24 @@ program
|
|
|
34
93
|
await repl.start();
|
|
35
94
|
});
|
|
36
95
|
program
|
|
37
|
-
.command('run
|
|
38
|
-
.description('Ejecutar un prompt directamente y esperar la respuesta')
|
|
39
|
-
.action(async (
|
|
96
|
+
.command('run [prompt...]')
|
|
97
|
+
.description('Ejecutar un prompt directamente y esperar la respuesta (soporta pipes: cat file | helix run)')
|
|
98
|
+
.action(async (promptParts) => {
|
|
99
|
+
let promptText = (promptParts || []).join(' ').trim();
|
|
100
|
+
if (!promptText || promptText === '-') {
|
|
101
|
+
promptText = await readStdin();
|
|
102
|
+
}
|
|
103
|
+
if (!promptText) {
|
|
104
|
+
console.error((0, colors_1.RED)('Error: Debes proporcionar un prompt como argumento o a través de stdin (ej: echo "hola" | helix run).'));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
40
107
|
const opts = program.opts();
|
|
41
108
|
const client = new agent_client_1.AgentClient({
|
|
42
109
|
wsUrl: opts.wsUrl || (0, env_1.getWsUrl)(),
|
|
43
110
|
wsToken: opts.token || (0, env_1.getWsToken)(),
|
|
44
111
|
autoReconnect: false,
|
|
45
112
|
});
|
|
113
|
+
console.log((0, colors_1.DIM)(`Prompt: ${promptText}\n`));
|
|
46
114
|
console.log((0, colors_1.CYAN)(`Conectando con HELIX en ${client.url}...`));
|
|
47
115
|
try {
|
|
48
116
|
await client.connect();
|
|
@@ -51,7 +119,6 @@ program
|
|
|
51
119
|
console.error((0, colors_1.RED)(`No se pudo conectar al agente HELIX: ${err.message}`));
|
|
52
120
|
process.exit(1);
|
|
53
121
|
}
|
|
54
|
-
console.log((0, colors_1.DIM)(`Prompt: ${promptText}\n`));
|
|
55
122
|
const convId = `cli_run_${Date.now()}`;
|
|
56
123
|
client.on('react_step', (step) => {
|
|
57
124
|
process.stdout.write(`\r${(0, colors_1.DIM)(`[ReAct Step ${step?.iteration || 1}...]`)}`);
|
package/dist/ui/banner.d.ts
CHANGED
|
@@ -7,18 +7,9 @@ export interface BannerStatusOptions {
|
|
|
7
7
|
activeTasksCount?: number;
|
|
8
8
|
}
|
|
9
9
|
export declare function getAsciiLogo(terminalWidth?: number): string;
|
|
10
|
-
/**
|
|
11
|
-
* Compact, badge-style status bar.
|
|
12
|
-
*/
|
|
13
10
|
export declare function getStatusBar(options: BannerStatusOptions): string;
|
|
14
|
-
/**
|
|
15
|
-
* Renders the persistent header. Clears screen only if clearScreen is explicitly true.
|
|
16
|
-
*/
|
|
17
11
|
export declare function renderHeader(options: BannerStatusOptions, clearScreen?: boolean): void;
|
|
18
|
-
/**
|
|
19
|
-
* Voice-capture indicator badge (for inline use in prompts).
|
|
20
|
-
*/
|
|
21
12
|
export declare function getVoiceIndicator(isCapturing: boolean, voiceEnabled: boolean): string;
|
|
22
|
-
export declare const HELIX_LOGO_ASCII:
|
|
23
|
-
export declare const HELIX_BIG_ART:
|
|
24
|
-
export declare const HELIX_HARD_BLOCKS:
|
|
13
|
+
export declare const HELIX_LOGO_ASCII: typeof getAsciiLogo;
|
|
14
|
+
export declare const HELIX_BIG_ART: typeof getAsciiLogo;
|
|
15
|
+
export declare const HELIX_HARD_BLOCKS: typeof getAsciiLogo;
|
package/dist/ui/banner.js
CHANGED
|
@@ -9,126 +9,27 @@ exports.getStatusBar = getStatusBar;
|
|
|
9
9
|
exports.renderHeader = renderHeader;
|
|
10
10
|
exports.getVoiceIndicator = getVoiceIndicator;
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
-
//
|
|
13
|
-
// HELIX — Pixel-art dot-matrix logo.
|
|
14
|
-
// Each letter is drawn on an 8-row × 5-col grid.
|
|
15
|
-
// "On" pixels → "██" block, "off" pixels → " " (space).
|
|
16
|
-
// Gradient runs top → bottom:
|
|
17
|
-
// rows 0-1 : bright white/cream
|
|
18
|
-
// rows 2-3 : bright yellow
|
|
19
|
-
// rows 4-5 : orange
|
|
20
|
-
// rows 6-7 : deep red
|
|
21
|
-
// Matches the pixel-art HELIX image with yellow→orange→red gradient.
|
|
22
|
-
// ═══════════════════════════════════════════════════════════════
|
|
23
|
-
// ── Letter bitmaps — 1=on, 0=off, 8 rows × 5 cols each ────────
|
|
24
|
-
const H = [
|
|
25
|
-
[1, 0, 0, 0, 1],
|
|
26
|
-
[1, 0, 0, 0, 1],
|
|
27
|
-
[1, 1, 1, 1, 1],
|
|
28
|
-
[1, 1, 1, 1, 1],
|
|
29
|
-
[1, 1, 1, 1, 1],
|
|
30
|
-
[1, 0, 0, 0, 1],
|
|
31
|
-
[1, 0, 0, 0, 1],
|
|
32
|
-
[1, 0, 0, 0, 1],
|
|
33
|
-
];
|
|
34
|
-
const E = [
|
|
35
|
-
[1, 1, 1, 1, 1],
|
|
36
|
-
[1, 0, 0, 0, 0],
|
|
37
|
-
[1, 1, 1, 1, 0],
|
|
38
|
-
[1, 1, 1, 1, 0],
|
|
39
|
-
[1, 1, 1, 1, 0],
|
|
40
|
-
[1, 0, 0, 0, 0],
|
|
41
|
-
[1, 0, 0, 0, 0],
|
|
42
|
-
[1, 1, 1, 1, 1],
|
|
43
|
-
];
|
|
44
|
-
const L = [
|
|
45
|
-
[1, 0, 0, 0, 0],
|
|
46
|
-
[1, 0, 0, 0, 0],
|
|
47
|
-
[1, 0, 0, 0, 0],
|
|
48
|
-
[1, 0, 0, 0, 0],
|
|
49
|
-
[1, 0, 0, 0, 0],
|
|
50
|
-
[1, 0, 0, 0, 0],
|
|
51
|
-
[1, 0, 0, 0, 0],
|
|
52
|
-
[1, 1, 1, 1, 1],
|
|
53
|
-
];
|
|
54
|
-
const I = [
|
|
55
|
-
[1, 1, 1, 1, 1],
|
|
56
|
-
[0, 0, 1, 0, 0],
|
|
57
|
-
[0, 0, 1, 0, 0],
|
|
58
|
-
[0, 0, 1, 0, 0],
|
|
59
|
-
[0, 0, 1, 0, 0],
|
|
60
|
-
[0, 0, 1, 0, 0],
|
|
61
|
-
[0, 0, 1, 0, 0],
|
|
62
|
-
[1, 1, 1, 1, 1],
|
|
63
|
-
];
|
|
64
|
-
const X = [
|
|
65
|
-
[1, 0, 0, 0, 1],
|
|
66
|
-
[1, 1, 0, 1, 1],
|
|
67
|
-
[0, 1, 1, 1, 0],
|
|
68
|
-
[0, 0, 1, 0, 0],
|
|
69
|
-
[0, 0, 1, 0, 0],
|
|
70
|
-
[0, 1, 1, 1, 0],
|
|
71
|
-
[1, 1, 0, 1, 1],
|
|
72
|
-
[1, 0, 0, 0, 1],
|
|
73
|
-
];
|
|
74
|
-
const LETTERS = [H, E, L, I, X];
|
|
75
|
-
const LETTER_GAP = 1; // 1 empty col between letters
|
|
76
|
-
const ROWS = 8;
|
|
77
|
-
const COLS_PER_LETTER = 5;
|
|
78
|
-
// ── Top-to-bottom gradient — cream/white → yellow → orange → red ──
|
|
79
|
-
// Row 0 = top, row 7 = bottom
|
|
80
|
-
const ROW_COLORS = [
|
|
81
|
-
'#FFF9E0', // 0 — near-white cream
|
|
82
|
-
'#FFE84D', // 1 — bright yellow
|
|
83
|
-
'#FFD200', // 2 — gold yellow
|
|
84
|
-
'#FFB300', // 3 — amber
|
|
85
|
-
'#FF8C00', // 4 — orange
|
|
86
|
-
'#FF6600', // 5 — deep orange
|
|
87
|
-
'#E63000', // 6 — red-orange
|
|
88
|
-
'#CC1A00', // 7 — deep red
|
|
89
|
-
];
|
|
90
|
-
const BLOCK_ON = '██'; // filled square block
|
|
91
|
-
const BLOCK_OFF = ' '; // empty (black background)
|
|
92
|
-
// ───────────────────────────────────────────────────────────────
|
|
93
|
-
// Public API
|
|
94
|
-
// ───────────────────────────────────────────────────────────────
|
|
12
|
+
// Exact bash logo with ANSI RGB codes - DO NOT MODIFY
|
|
95
13
|
function getAsciiLogo(terminalWidth = 80) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
else {
|
|
111
|
-
line += BLOCK_OFF;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// gap between letters
|
|
115
|
-
if (li < LETTERS.length - 1) {
|
|
116
|
-
line += BLOCK_OFF.repeat(LETTER_GAP);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
lines.push(line);
|
|
120
|
-
}
|
|
121
|
-
return lines.join('\n');
|
|
14
|
+
const lines = [
|
|
15
|
+
' \x1b[48;2;255;227;143m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;227;143m█ \x1b[48;2;255;227;143m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;227;143m█\x1b[0m',
|
|
16
|
+
' \x1b[38;2;255;210;77m██ ██ \x1b[38;2;255;255;255m██████ ██ ██ ██ ██\x1b[0m',
|
|
17
|
+
'\x1b[38;2;255;215;119m██ \x1b[38;2;255;191;31m██ ██ \x1b[38;2;255;215;119m██ \x1b[38;2;246;246;246m██ ██ ██ ██ ██\x1b[0m',
|
|
18
|
+
'\x1b[38;2;255;191;36m██ \x1b[38;2;255;171;10m███\x1b[48;2;255;171;10m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;171;10m███ \x1b[38;2;255;191;36m██ \x1b[38;2;236;236;236m████ ██ ██ ██\x1b[0m',
|
|
19
|
+
'\x1b[38;2;255;160;18m██ \x1b[38;2;255;145;20m███████ \x1b[38;2;255;160;18m██ \x1b[38;2;226;226;226m██ ██ ██ ██ ██\x1b[0m',
|
|
20
|
+
'\x1b[38;2;255;127;26m██ \x1b[38;2;255;122;26m██ ██ \x1b[38;2;255;127;26m██ \x1b[38;2;216;216;216m██████ ██████ ██ ██ ██\x1b[0m',
|
|
21
|
+
'\x1b[38;2;240;98;15m██ \x1b[38;2;255;95;18m██ ██ \x1b[38;2;240;98;15m██\x1b[0m',
|
|
22
|
+
' \x1b[38;2;220;74;12m██ ██\x1b[0m'
|
|
23
|
+
];
|
|
24
|
+
const visibleWidth = 62;
|
|
25
|
+
const leftPad = Math.max(0, Math.floor((terminalWidth - visibleWidth) / 2));
|
|
26
|
+
const padding = ' '.repeat(leftPad);
|
|
27
|
+
return lines.map(line => padding + line).join('\n');
|
|
122
28
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Compact, badge-style status bar.
|
|
125
|
-
*/
|
|
126
29
|
function getStatusBar(options) {
|
|
127
|
-
// ── Connection ──
|
|
128
30
|
const conn = options.connected
|
|
129
31
|
? `${chalk_1.default.green('●')} ${chalk_1.default.greenBright.bold('CONECTADO')} ${chalk_1.default.gray(options.wsUrl)}`
|
|
130
32
|
: `${chalk_1.default.red('●')} ${chalk_1.default.redBright.bold('DESCONECTADO')} ${chalk_1.default.gray(options.wsUrl)}`;
|
|
131
|
-
// ── Microphone ──
|
|
132
33
|
let mic;
|
|
133
34
|
if (options.isCapturingVoice) {
|
|
134
35
|
mic = chalk_1.default.bgRed.white.bold(' 🎙️ CAPTURANDO ');
|
|
@@ -139,42 +40,26 @@ function getStatusBar(options) {
|
|
|
139
40
|
else {
|
|
140
41
|
mic = `${chalk_1.default.red('●')} ${chalk_1.default.gray('Mic Off')}`;
|
|
141
42
|
}
|
|
142
|
-
// ── Model ──
|
|
143
43
|
const model = options.currentModel
|
|
144
44
|
? `${chalk_1.default.hex('#FF9F1C')('⚡')} ${chalk_1.default.white(options.currentModel)}`
|
|
145
45
|
: `${chalk_1.default.hex('#FF9F1C')('⚡')} ${chalk_1.default.white('Local ReAct')}`;
|
|
146
|
-
// ── Active tasks ──
|
|
147
46
|
const tasks = (options.activeTasksCount ?? 0) > 0
|
|
148
47
|
? ` ${chalk_1.default.yellow('⏳')} ${chalk_1.default.yellowBright(String(options.activeTasksCount))}`
|
|
149
48
|
: '';
|
|
150
49
|
const sep = chalk_1.default.gray(' │ ');
|
|
151
50
|
return ` ${conn}${sep}${mic}${sep}${model}${tasks}`;
|
|
152
51
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Renders the persistent header. Clears screen only if clearScreen is explicitly true.
|
|
155
|
-
*/
|
|
156
52
|
function renderHeader(options, clearScreen = false) {
|
|
157
53
|
if (clearScreen) {
|
|
158
|
-
// Clear screen + move cursor to top-left
|
|
159
54
|
process.stdout.write('\x1bc');
|
|
160
55
|
}
|
|
161
56
|
const width = process.stdout.columns || 80;
|
|
162
|
-
// ── Logo ──
|
|
163
57
|
const logo = getAsciiLogo(width);
|
|
164
|
-
// ── Tagline ──
|
|
165
58
|
const tagline = 'Persistent AI Desktop Agent';
|
|
166
59
|
const tagPad = Math.max(0, Math.floor((width - tagline.length - 8) / 2));
|
|
167
|
-
const subtitle = ' '.repeat(tagPad) +
|
|
168
|
-
chalk_1.default.gray('╶───') +
|
|
169
|
-
' ' +
|
|
170
|
-
chalk_1.default.hex('#FF9F1C')(tagline) +
|
|
171
|
-
' ' +
|
|
172
|
-
chalk_1.default.gray('───╴');
|
|
173
|
-
// ── Status bar ──
|
|
60
|
+
const subtitle = ' '.repeat(tagPad) + chalk_1.default.gray('╶───') + ' ' + chalk_1.default.hex('#FF9F1C')(tagline) + ' ' + chalk_1.default.gray('───╴');
|
|
174
61
|
const statusBar = getStatusBar(options);
|
|
175
|
-
// ── Visual lines ──
|
|
176
62
|
const thinLine = chalk_1.default.gray('─'.repeat(width));
|
|
177
|
-
// ── Render ──
|
|
178
63
|
console.log('');
|
|
179
64
|
console.log(logo);
|
|
180
65
|
console.log('');
|
|
@@ -183,25 +68,17 @@ function renderHeader(options, clearScreen = false) {
|
|
|
183
68
|
console.log(thinLine);
|
|
184
69
|
console.log(statusBar);
|
|
185
70
|
console.log(thinLine);
|
|
186
|
-
console.log(` ${chalk_1.default.hex('#FF8C00')('@skill')} ${chalk_1.default.gray('Skills')} ${chalk_1.default.gray('│')} `
|
|
187
|
-
`${chalk_1.default.cyan('#mcp')} ${chalk_1.default.gray('Herramientas')} ${chalk_1.default.gray('│')} ` +
|
|
188
|
-
`${chalk_1.default.magenta('/help')} ${chalk_1.default.gray('Comandos')}`);
|
|
71
|
+
console.log(` ${chalk_1.default.hex('#FF8C00')('@skill')} ${chalk_1.default.gray('Skills')} ${chalk_1.default.gray('│')} ${chalk_1.default.cyan('#mcp')} ${chalk_1.default.gray('Herramientas')} ${chalk_1.default.gray('│')} ${chalk_1.default.magenta('/help')} ${chalk_1.default.gray('Comandos')}`);
|
|
189
72
|
console.log(thinLine);
|
|
190
73
|
console.log('');
|
|
191
74
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Voice-capture indicator badge (for inline use in prompts).
|
|
194
|
-
*/
|
|
195
75
|
function getVoiceIndicator(isCapturing, voiceEnabled) {
|
|
196
|
-
if (isCapturing)
|
|
76
|
+
if (isCapturing)
|
|
197
77
|
return chalk_1.default.bgRed.white.bold(' 🎙️ VOZ DETECTADA [HEY HELIX] 🔊 ');
|
|
198
|
-
|
|
199
|
-
if (voiceEnabled) {
|
|
78
|
+
if (voiceEnabled)
|
|
200
79
|
return chalk_1.default.green('🟢 [Mic Activo]');
|
|
201
|
-
}
|
|
202
80
|
return chalk_1.default.red('🔴 [Mic Inactivo]');
|
|
203
81
|
}
|
|
204
|
-
|
|
205
|
-
exports.
|
|
206
|
-
exports.
|
|
207
|
-
exports.HELIX_HARD_BLOCKS = LETTERS;
|
|
82
|
+
exports.HELIX_LOGO_ASCII = getAsciiLogo;
|
|
83
|
+
exports.HELIX_BIG_ART = getAsciiLogo;
|
|
84
|
+
exports.HELIX_HARD_BLOCKS = getAsciiLogo;
|
package/dist/ui/repl.js
CHANGED
|
@@ -122,7 +122,7 @@ class HelixRepl {
|
|
|
122
122
|
// Voice Capture indicator
|
|
123
123
|
this.client.on('voice_capture_started', () => {
|
|
124
124
|
this.isCapturingVoice = true;
|
|
125
|
-
process.stdout.write(`\n${chalk_1.default.bgRed.white.bold(' 🎙️
|
|
125
|
+
process.stdout.write(`\n${chalk_1.default.bgRed.white.bold(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
|
|
126
126
|
});
|
|
127
127
|
this.client.on('voice_capture_stopped', () => {
|
|
128
128
|
this.isCapturingVoice = false;
|
|
@@ -171,7 +171,7 @@ class HelixRepl {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
async _handleConfirmationDialog(req) {
|
|
174
|
-
console.log(`\n${(0, colors_1.YELLOW_BOLD)('⚠️
|
|
174
|
+
console.log(`\n${(0, colors_1.YELLOW_BOLD)('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
|
|
175
175
|
console.log(` Acción: ${chalk_1.default.white.bold(req.action || req.tool || 'Acción del sistema')}`);
|
|
176
176
|
if (req.description) {
|
|
177
177
|
console.log(` Descripción: ${(0, colors_1.DIM)(req.description)}`);
|
|
@@ -179,29 +179,45 @@ class HelixRepl {
|
|
|
179
179
|
if (req.arguments && Object.keys(req.arguments).length > 0) {
|
|
180
180
|
console.log(` Parámetros: ${(0, colors_1.GRAY)(JSON.stringify(req.arguments))}`);
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
182
|
+
this.rl?.pause();
|
|
183
|
+
try {
|
|
184
|
+
const answer = await (0, prompts_1.default)({
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'confirmed',
|
|
187
|
+
message: '¿Permitir al agente ejecutar esta acción?',
|
|
188
|
+
initial: true,
|
|
189
|
+
});
|
|
190
|
+
const reqId = req.request_id || req.id || '';
|
|
191
|
+
if (answer && answer.confirmed) {
|
|
192
|
+
this.client.confirmAction(reqId);
|
|
193
|
+
console.log((0, colors_1.GREEN)(' ✔ Acción confirmada.\n'));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this.client.rejectAction(reqId);
|
|
197
|
+
console.log((0, colors_1.RED)(' ✖ Acción rechazada.\n'));
|
|
198
|
+
}
|
|
192
199
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
finally {
|
|
201
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
process.stdin.setRawMode(false);
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
}
|
|
207
|
+
this.rl?.resume();
|
|
208
|
+
this._promptUser();
|
|
196
209
|
}
|
|
197
|
-
this._promptUser();
|
|
198
210
|
}
|
|
199
211
|
_startReadline() {
|
|
212
|
+
if (process.stdin.setEncoding) {
|
|
213
|
+
process.stdin.setEncoding('utf-8');
|
|
214
|
+
}
|
|
215
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
200
216
|
const rl = readline_1.default.createInterface({
|
|
201
217
|
input: process.stdin,
|
|
202
218
|
output: process.stdout,
|
|
203
219
|
completer: (line) => this.autocompleter.completer(line),
|
|
204
|
-
terminal:
|
|
220
|
+
terminal: isInteractive,
|
|
205
221
|
});
|
|
206
222
|
this.rl = rl;
|
|
207
223
|
rl.on('line', async (line) => {
|
|
@@ -260,8 +276,18 @@ class HelixRepl {
|
|
|
260
276
|
break;
|
|
261
277
|
case '/config-apikey':
|
|
262
278
|
this.rl?.pause();
|
|
263
|
-
|
|
264
|
-
|
|
279
|
+
try {
|
|
280
|
+
await (0, apikey_1.handleConfigApiKey)(this.client);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
284
|
+
try {
|
|
285
|
+
process.stdin.setRawMode(false);
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
}
|
|
289
|
+
this.rl?.resume();
|
|
290
|
+
}
|
|
265
291
|
break;
|
|
266
292
|
case '/show-apikey':
|
|
267
293
|
await (0, apikey_1.handleShowApiKey)(this.client);
|
|
@@ -271,18 +297,38 @@ class HelixRepl {
|
|
|
271
297
|
break;
|
|
272
298
|
case '/create-skill':
|
|
273
299
|
this.rl?.pause();
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
try {
|
|
301
|
+
await (0, skills_1.handleCreateSkill)(this.client);
|
|
302
|
+
await this.autocompleter.refreshSuggestions();
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
306
|
+
try {
|
|
307
|
+
process.stdin.setRawMode(false);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
}
|
|
311
|
+
this.rl?.resume();
|
|
312
|
+
}
|
|
277
313
|
break;
|
|
278
314
|
case '/mcp':
|
|
279
315
|
await (0, mcp_1.handleListMcp)(this.client);
|
|
280
316
|
break;
|
|
281
317
|
case '/create-mcp':
|
|
282
318
|
this.rl?.pause();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
319
|
+
try {
|
|
320
|
+
await (0, mcp_1.handleCreateMcp)(this.client);
|
|
321
|
+
await this.autocompleter.refreshSuggestions();
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
325
|
+
try {
|
|
326
|
+
process.stdin.setRawMode(false);
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
330
|
+
this.rl?.resume();
|
|
331
|
+
}
|
|
286
332
|
break;
|
|
287
333
|
case '/microphone': {
|
|
288
334
|
const newState = await (0, mic_1.handleMicrophoneCommand)(this.client, args, this.voiceEnabled);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helix-assistant-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "HELIX - Interactive Terminal CLI for Windows AI Desktop Agent",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"@types/ws": "^8.5.10",
|
|
31
31
|
"typescript": "^5.3.0"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
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';
|
|
1
27
|
import { Command } from 'commander';
|
|
2
28
|
import { HelixRepl } from './ui/repl';
|
|
3
29
|
import { getWsUrl, getWsToken } from './config/env';
|
|
@@ -7,6 +33,41 @@ import { handleShowApiKey } from './commands/apikey';
|
|
|
7
33
|
import { ORANGE, CYAN, GREEN, RED, BOLD, DIM } from './ui/colors';
|
|
8
34
|
import { getAsciiLogo } from './ui/banner';
|
|
9
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
|
+
|
|
10
71
|
const program = new Command();
|
|
11
72
|
|
|
12
73
|
program
|
|
@@ -37,9 +98,17 @@ program
|
|
|
37
98
|
});
|
|
38
99
|
|
|
39
100
|
program
|
|
40
|
-
.command('run
|
|
41
|
-
.description('Ejecutar un prompt directamente y esperar la respuesta')
|
|
42
|
-
.action(async (
|
|
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
|
+
}
|
|
43
112
|
const opts = program.opts();
|
|
44
113
|
const client = new AgentClient({
|
|
45
114
|
wsUrl: opts.wsUrl || getWsUrl(),
|
|
@@ -47,6 +116,7 @@ program
|
|
|
47
116
|
autoReconnect: false,
|
|
48
117
|
});
|
|
49
118
|
|
|
119
|
+
console.log(DIM(`Prompt: ${promptText}\n`));
|
|
50
120
|
console.log(CYAN(`Conectando con HELIX en ${client.url}...`));
|
|
51
121
|
try {
|
|
52
122
|
await client.connect();
|
|
@@ -55,7 +125,6 @@ program
|
|
|
55
125
|
process.exit(1);
|
|
56
126
|
}
|
|
57
127
|
|
|
58
|
-
console.log(DIM(`Prompt: ${promptText}\n`));
|
|
59
128
|
const convId = `cli_run_${Date.now()}`;
|
|
60
129
|
|
|
61
130
|
client.on('react_step', (step: { iteration?: number }) => {
|
package/src/ui/banner.ts
CHANGED
|
@@ -10,145 +10,28 @@ export interface BannerStatusOptions {
|
|
|
10
10
|
activeTasksCount?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
// HELIX — Pixel-art dot-matrix logo.
|
|
15
|
-
// Each letter is drawn on an 8-row × 5-col grid.
|
|
16
|
-
// "On" pixels → "██" block, "off" pixels → " " (space).
|
|
17
|
-
// Gradient runs top → bottom:
|
|
18
|
-
// rows 0-1 : bright white/cream
|
|
19
|
-
// rows 2-3 : bright yellow
|
|
20
|
-
// rows 4-5 : orange
|
|
21
|
-
// rows 6-7 : deep red
|
|
22
|
-
// Matches the pixel-art HELIX image with yellow→orange→red gradient.
|
|
23
|
-
// ═══════════════════════════════════════════════════════════════
|
|
24
|
-
|
|
25
|
-
// ── Letter bitmaps — 1=on, 0=off, 8 rows × 5 cols each ────────
|
|
26
|
-
|
|
27
|
-
const H: number[][] = [
|
|
28
|
-
[1,0,0,0,1],
|
|
29
|
-
[1,0,0,0,1],
|
|
30
|
-
[1,1,1,1,1],
|
|
31
|
-
[1,1,1,1,1],
|
|
32
|
-
[1,1,1,1,1],
|
|
33
|
-
[1,0,0,0,1],
|
|
34
|
-
[1,0,0,0,1],
|
|
35
|
-
[1,0,0,0,1],
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const E: number[][] = [
|
|
39
|
-
[1,1,1,1,1],
|
|
40
|
-
[1,0,0,0,0],
|
|
41
|
-
[1,1,1,1,0],
|
|
42
|
-
[1,1,1,1,0],
|
|
43
|
-
[1,1,1,1,0],
|
|
44
|
-
[1,0,0,0,0],
|
|
45
|
-
[1,0,0,0,0],
|
|
46
|
-
[1,1,1,1,1],
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const L: number[][] = [
|
|
50
|
-
[1,0,0,0,0],
|
|
51
|
-
[1,0,0,0,0],
|
|
52
|
-
[1,0,0,0,0],
|
|
53
|
-
[1,0,0,0,0],
|
|
54
|
-
[1,0,0,0,0],
|
|
55
|
-
[1,0,0,0,0],
|
|
56
|
-
[1,0,0,0,0],
|
|
57
|
-
[1,1,1,1,1],
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const I: number[][] = [
|
|
61
|
-
[1,1,1,1,1],
|
|
62
|
-
[0,0,1,0,0],
|
|
63
|
-
[0,0,1,0,0],
|
|
64
|
-
[0,0,1,0,0],
|
|
65
|
-
[0,0,1,0,0],
|
|
66
|
-
[0,0,1,0,0],
|
|
67
|
-
[0,0,1,0,0],
|
|
68
|
-
[1,1,1,1,1],
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const X: number[][] = [
|
|
72
|
-
[1,0,0,0,1],
|
|
73
|
-
[1,1,0,1,1],
|
|
74
|
-
[0,1,1,1,0],
|
|
75
|
-
[0,0,1,0,0],
|
|
76
|
-
[0,0,1,0,0],
|
|
77
|
-
[0,1,1,1,0],
|
|
78
|
-
[1,1,0,1,1],
|
|
79
|
-
[1,0,0,0,1],
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
const LETTERS = [H, E, L, I, X];
|
|
83
|
-
const LETTER_GAP = 1; // 1 empty col between letters
|
|
84
|
-
const ROWS = 8;
|
|
85
|
-
const COLS_PER_LETTER = 5;
|
|
86
|
-
|
|
87
|
-
// ── Top-to-bottom gradient — cream/white → yellow → orange → red ──
|
|
88
|
-
// Row 0 = top, row 7 = bottom
|
|
89
|
-
const ROW_COLORS: string[] = [
|
|
90
|
-
'#FFF9E0', // 0 — near-white cream
|
|
91
|
-
'#FFE84D', // 1 — bright yellow
|
|
92
|
-
'#FFD200', // 2 — gold yellow
|
|
93
|
-
'#FFB300', // 3 — amber
|
|
94
|
-
'#FF8C00', // 4 — orange
|
|
95
|
-
'#FF6600', // 5 — deep orange
|
|
96
|
-
'#E63000', // 6 — red-orange
|
|
97
|
-
'#CC1A00', // 7 — deep red
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
const BLOCK_ON = '██'; // filled square block
|
|
101
|
-
const BLOCK_OFF = ' '; // empty (black background)
|
|
102
|
-
|
|
103
|
-
// ───────────────────────────────────────────────────────────────
|
|
104
|
-
// Public API
|
|
105
|
-
// ───────────────────────────────────────────────────────────────
|
|
106
|
-
|
|
13
|
+
// Exact bash logo with ANSI RGB codes - DO NOT MODIFY
|
|
107
14
|
export function getAsciiLogo(terminalWidth: number = 80): string {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (let col = 0; col < COLS_PER_LETTER; col++) {
|
|
123
|
-
if (letter[row][col]) {
|
|
124
|
-
line += chalk.hex(ROW_COLORS[row]).bold(BLOCK_ON);
|
|
125
|
-
} else {
|
|
126
|
-
line += BLOCK_OFF;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// gap between letters
|
|
131
|
-
if (li < LETTERS.length - 1) {
|
|
132
|
-
line += BLOCK_OFF.repeat(LETTER_GAP);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
lines.push(line);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return lines.join('\n');
|
|
15
|
+
const lines = [
|
|
16
|
+
' \x1b[48;2;255;227;143m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;227;143m█ \x1b[48;2;255;227;143m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;227;143m█\x1b[0m',
|
|
17
|
+
' \x1b[38;2;255;210;77m██ ██ \x1b[38;2;255;255;255m██████ ██ ██ ██ ██\x1b[0m',
|
|
18
|
+
'\x1b[38;2;255;215;119m██ \x1b[38;2;255;191;31m██ ██ \x1b[38;2;255;215;119m██ \x1b[38;2;246;246;246m██ ██ ██ ██ ██\x1b[0m',
|
|
19
|
+
'\x1b[38;2;255;191;36m██ \x1b[38;2;255;171;10m███\x1b[48;2;255;171;10m\x1b[38;2;255;255;255m▀\x1b[49m\x1b[38;2;255;171;10m███ \x1b[38;2;255;191;36m██ \x1b[38;2;236;236;236m████ ██ ██ ██\x1b[0m',
|
|
20
|
+
'\x1b[38;2;255;160;18m██ \x1b[38;2;255;145;20m███████ \x1b[38;2;255;160;18m██ \x1b[38;2;226;226;226m██ ██ ██ ██ ██\x1b[0m',
|
|
21
|
+
'\x1b[38;2;255;127;26m██ \x1b[38;2;255;122;26m██ ██ \x1b[38;2;255;127;26m██ \x1b[38;2;216;216;216m██████ ██████ ██ ██ ██\x1b[0m',
|
|
22
|
+
'\x1b[38;2;240;98;15m██ \x1b[38;2;255;95;18m██ ██ \x1b[38;2;240;98;15m██\x1b[0m',
|
|
23
|
+
' \x1b[38;2;220;74;12m██ ██\x1b[0m'
|
|
24
|
+
];
|
|
25
|
+
const visibleWidth = 62;
|
|
26
|
+
const leftPad = Math.max(0, Math.floor((terminalWidth - visibleWidth) / 2));
|
|
27
|
+
const padding = ' '.repeat(leftPad);
|
|
28
|
+
return lines.map(line => padding + line).join('\n');
|
|
140
29
|
}
|
|
141
30
|
|
|
142
|
-
/**
|
|
143
|
-
* Compact, badge-style status bar.
|
|
144
|
-
*/
|
|
145
31
|
export function getStatusBar(options: BannerStatusOptions): string {
|
|
146
|
-
// ── Connection ──
|
|
147
32
|
const conn = options.connected
|
|
148
33
|
? `${chalk.green('●')} ${chalk.greenBright.bold('CONECTADO')} ${chalk.gray(options.wsUrl)}`
|
|
149
34
|
: `${chalk.red('●')} ${chalk.redBright.bold('DESCONECTADO')} ${chalk.gray(options.wsUrl)}`;
|
|
150
|
-
|
|
151
|
-
// ── Microphone ──
|
|
152
35
|
let mic: string;
|
|
153
36
|
if (options.isCapturingVoice) {
|
|
154
37
|
mic = chalk.bgRed.white.bold(' 🎙️ CAPTURANDO ');
|
|
@@ -157,53 +40,27 @@ export function getStatusBar(options: BannerStatusOptions): string {
|
|
|
157
40
|
} else {
|
|
158
41
|
mic = `${chalk.red('●')} ${chalk.gray('Mic Off')}`;
|
|
159
42
|
}
|
|
160
|
-
|
|
161
|
-
// ── Model ──
|
|
162
43
|
const model = options.currentModel
|
|
163
44
|
? `${chalk.hex('#FF9F1C')('⚡')} ${chalk.white(options.currentModel)}`
|
|
164
45
|
: `${chalk.hex('#FF9F1C')('⚡')} ${chalk.white('Local ReAct')}`;
|
|
165
|
-
|
|
166
|
-
// ── Active tasks ──
|
|
167
46
|
const tasks = (options.activeTasksCount ?? 0) > 0
|
|
168
47
|
? ` ${chalk.yellow('⏳')} ${chalk.yellowBright(String(options.activeTasksCount))}`
|
|
169
48
|
: '';
|
|
170
|
-
|
|
171
49
|
const sep = chalk.gray(' │ ');
|
|
172
50
|
return ` ${conn}${sep}${mic}${sep}${model}${tasks}`;
|
|
173
51
|
}
|
|
174
52
|
|
|
175
|
-
/**
|
|
176
|
-
* Renders the persistent header. Clears screen only if clearScreen is explicitly true.
|
|
177
|
-
*/
|
|
178
53
|
export function renderHeader(options: BannerStatusOptions, clearScreen: boolean = false): void {
|
|
179
54
|
if (clearScreen) {
|
|
180
|
-
// Clear screen + move cursor to top-left
|
|
181
55
|
process.stdout.write('\x1bc');
|
|
182
56
|
}
|
|
183
|
-
|
|
184
57
|
const width = process.stdout.columns || 80;
|
|
185
|
-
|
|
186
|
-
// ── Logo ──
|
|
187
58
|
const logo = getAsciiLogo(width);
|
|
188
|
-
|
|
189
|
-
// ── Tagline ──
|
|
190
59
|
const tagline = 'Persistent AI Desktop Agent';
|
|
191
60
|
const tagPad = Math.max(0, Math.floor((width - tagline.length - 8) / 2));
|
|
192
|
-
const subtitle =
|
|
193
|
-
' '.repeat(tagPad) +
|
|
194
|
-
chalk.gray('╶───') +
|
|
195
|
-
' ' +
|
|
196
|
-
chalk.hex('#FF9F1C')(tagline) +
|
|
197
|
-
' ' +
|
|
198
|
-
chalk.gray('───╴');
|
|
199
|
-
|
|
200
|
-
// ── Status bar ──
|
|
61
|
+
const subtitle = ' '.repeat(tagPad) + chalk.gray('╶───') + ' ' + chalk.hex('#FF9F1C')(tagline) + ' ' + chalk.gray('───╴');
|
|
201
62
|
const statusBar = getStatusBar(options);
|
|
202
|
-
|
|
203
|
-
// ── Visual lines ──
|
|
204
63
|
const thinLine = chalk.gray('─'.repeat(width));
|
|
205
|
-
|
|
206
|
-
// ── Render ──
|
|
207
64
|
console.log('');
|
|
208
65
|
console.log(logo);
|
|
209
66
|
console.log('');
|
|
@@ -212,29 +69,17 @@ export function renderHeader(options: BannerStatusOptions, clearScreen: boolean
|
|
|
212
69
|
console.log(thinLine);
|
|
213
70
|
console.log(statusBar);
|
|
214
71
|
console.log(thinLine);
|
|
215
|
-
console.log(
|
|
216
|
-
` ${chalk.hex('#FF8C00')('@skill')} ${chalk.gray('Skills')} ${chalk.gray('│')} ` +
|
|
217
|
-
`${chalk.cyan('#mcp')} ${chalk.gray('Herramientas')} ${chalk.gray('│')} ` +
|
|
218
|
-
`${chalk.magenta('/help')} ${chalk.gray('Comandos')}`
|
|
219
|
-
);
|
|
72
|
+
console.log(` ${chalk.hex('#FF8C00')('@skill')} ${chalk.gray('Skills')} ${chalk.gray('│')} ${chalk.cyan('#mcp')} ${chalk.gray('Herramientas')} ${chalk.gray('│')} ${chalk.magenta('/help')} ${chalk.gray('Comandos')}`);
|
|
220
73
|
console.log(thinLine);
|
|
221
74
|
console.log('');
|
|
222
75
|
}
|
|
223
76
|
|
|
224
|
-
/**
|
|
225
|
-
* Voice-capture indicator badge (for inline use in prompts).
|
|
226
|
-
*/
|
|
227
77
|
export function getVoiceIndicator(isCapturing: boolean, voiceEnabled: boolean): string {
|
|
228
|
-
if (isCapturing)
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (voiceEnabled) {
|
|
232
|
-
return chalk.green('🟢 [Mic Activo]');
|
|
233
|
-
}
|
|
78
|
+
if (isCapturing) return chalk.bgRed.white.bold(' 🎙️ VOZ DETECTADA [HEY HELIX] 🔊 ');
|
|
79
|
+
if (voiceEnabled) return chalk.green('🟢 [Mic Activo]');
|
|
234
80
|
return chalk.red('🔴 [Mic Inactivo]');
|
|
235
81
|
}
|
|
236
82
|
|
|
237
|
-
|
|
238
|
-
export const
|
|
239
|
-
export const
|
|
240
|
-
export const HELIX_HARD_BLOCKS = LETTERS;
|
|
83
|
+
export const HELIX_LOGO_ASCII = getAsciiLogo;
|
|
84
|
+
export const HELIX_BIG_ART = getAsciiLogo;
|
|
85
|
+
export const HELIX_HARD_BLOCKS = getAsciiLogo;
|
package/src/ui/repl.ts
CHANGED
|
@@ -148,7 +148,7 @@ export class HelixRepl {
|
|
|
148
148
|
// Voice Capture indicator
|
|
149
149
|
this.client.on('voice_capture_started', () => {
|
|
150
150
|
this.isCapturingVoice = true;
|
|
151
|
-
process.stdout.write(`\n${chalk.bgRed.white.bold(' 🎙️
|
|
151
|
+
process.stdout.write(`\n${chalk.bgRed.white.bold(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
this.client.on('voice_capture_stopped', () => {
|
|
@@ -207,7 +207,7 @@ export class HelixRepl {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
private async _handleConfirmationDialog(req: ConfirmationRequest): Promise<void> {
|
|
210
|
-
console.log(`\n${YELLOW_BOLD('⚠️
|
|
210
|
+
console.log(`\n${YELLOW_BOLD('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
|
|
211
211
|
console.log(` Acción: ${chalk.white.bold(req.action || req.tool || 'Acción del sistema')}`);
|
|
212
212
|
if (req.description) {
|
|
213
213
|
console.log(` Descripción: ${DIM(req.description)}`);
|
|
@@ -216,31 +216,44 @@ export class HelixRepl {
|
|
|
216
216
|
console.log(` Parámetros: ${GRAY(JSON.stringify(req.arguments))}`);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
219
|
+
this.rl?.pause();
|
|
220
|
+
try {
|
|
221
|
+
const answer = await prompts({
|
|
222
|
+
type: 'confirm',
|
|
223
|
+
name: 'confirmed',
|
|
224
|
+
message: '¿Permitir al agente ejecutar esta acción?',
|
|
225
|
+
initial: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const reqId = req.request_id || req.id || '';
|
|
229
|
+
if (answer && answer.confirmed) {
|
|
230
|
+
this.client.confirmAction(reqId);
|
|
231
|
+
console.log(GREEN(' ✔ Acción confirmada.\n'));
|
|
232
|
+
} else {
|
|
233
|
+
this.client.rejectAction(reqId);
|
|
234
|
+
console.log(RED(' ✖ Acción rechazada.\n'));
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
238
|
+
try {
|
|
239
|
+
process.stdin.setRawMode(false);
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
this.rl?.resume();
|
|
243
|
+
this._promptUser();
|
|
233
244
|
}
|
|
234
|
-
|
|
235
|
-
this._promptUser();
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
private _startReadline(): void {
|
|
248
|
+
if (process.stdin.setEncoding) {
|
|
249
|
+
process.stdin.setEncoding('utf-8');
|
|
250
|
+
}
|
|
251
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
239
252
|
const rl = readline.createInterface({
|
|
240
253
|
input: process.stdin,
|
|
241
254
|
output: process.stdout,
|
|
242
255
|
completer: (line: string) => this.autocompleter.completer(line),
|
|
243
|
-
terminal:
|
|
256
|
+
terminal: isInteractive,
|
|
244
257
|
});
|
|
245
258
|
|
|
246
259
|
this.rl = rl;
|
|
@@ -309,8 +322,14 @@ export class HelixRepl {
|
|
|
309
322
|
|
|
310
323
|
case '/config-apikey':
|
|
311
324
|
this.rl?.pause();
|
|
312
|
-
|
|
313
|
-
|
|
325
|
+
try {
|
|
326
|
+
await handleConfigApiKey(this.client);
|
|
327
|
+
} finally {
|
|
328
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
329
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
330
|
+
}
|
|
331
|
+
this.rl?.resume();
|
|
332
|
+
}
|
|
314
333
|
break;
|
|
315
334
|
|
|
316
335
|
case '/show-apikey':
|
|
@@ -323,9 +342,15 @@ export class HelixRepl {
|
|
|
323
342
|
|
|
324
343
|
case '/create-skill':
|
|
325
344
|
this.rl?.pause();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
345
|
+
try {
|
|
346
|
+
await handleCreateSkill(this.client);
|
|
347
|
+
await this.autocompleter.refreshSuggestions();
|
|
348
|
+
} finally {
|
|
349
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
350
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
351
|
+
}
|
|
352
|
+
this.rl?.resume();
|
|
353
|
+
}
|
|
329
354
|
break;
|
|
330
355
|
|
|
331
356
|
case '/mcp':
|
|
@@ -334,9 +359,15 @@ export class HelixRepl {
|
|
|
334
359
|
|
|
335
360
|
case '/create-mcp':
|
|
336
361
|
this.rl?.pause();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
362
|
+
try {
|
|
363
|
+
await handleCreateMcp(this.client);
|
|
364
|
+
await this.autocompleter.refreshSuggestions();
|
|
365
|
+
} finally {
|
|
366
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
367
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
368
|
+
}
|
|
369
|
+
this.rl?.resume();
|
|
370
|
+
}
|
|
340
371
|
break;
|
|
341
372
|
|
|
342
373
|
case '/microphone': {
|
package/tests/cli.test.ts
CHANGED
|
@@ -139,3 +139,49 @@ test('Launcher: probePort resolves false on closed random port', async () => {
|
|
|
139
139
|
const pythonExec = findPythonExecutable(process.cwd());
|
|
140
140
|
assert.ok(pythonExec.length > 0);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
test('CLI: run without arguments prints helpful error and exits with code 1', async () => {
|
|
144
|
+
const { execSync } = await import('node:child_process');
|
|
145
|
+
const path = await import('node:path');
|
|
146
|
+
const binPath = path.resolve(__dirname, '../bin/helix.js');
|
|
147
|
+
try {
|
|
148
|
+
execSync(`node "${binPath}" run`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
149
|
+
assert.fail('Should have exited with code 1');
|
|
150
|
+
} catch (err: any) {
|
|
151
|
+
assert.equal(err.status, 1);
|
|
152
|
+
const stderr = err.stderr.toString('utf-8');
|
|
153
|
+
assert.ok(stderr.includes('Debes proporcionar un prompt'));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('CLI: run with piped UTF-8 Spanish text with BOM is decoded cleanly', async () => {
|
|
158
|
+
const { spawn } = await import('node:child_process');
|
|
159
|
+
const path = await import('node:path');
|
|
160
|
+
const binPath = path.resolve(__dirname, '../bin/helix.js');
|
|
161
|
+
|
|
162
|
+
const child = spawn('node', [binPath, 'run'], {
|
|
163
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Write UTF-8 BOM + Spanish text with accents
|
|
167
|
+
const payload = Buffer.concat([
|
|
168
|
+
Buffer.from([0xef, 0xbb, 0xbf]),
|
|
169
|
+
Buffer.from('¿Cuál es la configuración actual?', 'utf-8'),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
child.stdin.write(payload);
|
|
173
|
+
child.stdin.end();
|
|
174
|
+
|
|
175
|
+
let stdout = '';
|
|
176
|
+
let stderr = '';
|
|
177
|
+
child.stdout.on('data', (d) => { stdout += d.toString('utf-8'); });
|
|
178
|
+
child.stderr.on('data', (d) => { stderr += d.toString('utf-8'); });
|
|
179
|
+
|
|
180
|
+
await new Promise<void>((resolve) => {
|
|
181
|
+
child.on('close', () => {
|
|
182
|
+
assert.ok(stdout.includes('¿Cuál es la configuración actual?'));
|
|
183
|
+
resolve();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
package/tsconfig.json
CHANGED
|
@@ -9,10 +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
|
|
12
13
|
"moduleResolution": "node",
|
|
13
14
|
"resolveJsonModule": true,
|
|
14
15
|
"declaration": true
|
|
15
16
|
},
|
|
16
17
|
"include": ["src/**/*"],
|
|
17
18
|
"exclude": ["node_modules", "dist"]
|
|
18
|
-
}
|
|
19
|
+
}
|