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 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 <prompt>')
38
- .description('Ejecutar un prompt directamente y esperar la respuesta')
39
- .action(async (promptText) => {
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}...]`)}`);
@@ -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: number[][][];
23
- export declare const HELIX_BIG_ART: number[][][];
24
- export declare const HELIX_HARD_BLOCKS: number[][][];
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
- // total character width: each cell = 2 chars
97
- const totalCols = LETTERS.length * COLS_PER_LETTER + (LETTERS.length - 1) * LETTER_GAP;
98
- const totalChars = totalCols * 2;
99
- const leftPad = Math.max(0, Math.floor((terminalWidth - totalChars) / 2));
100
- const pad = ' '.repeat(leftPad);
101
- const lines = [];
102
- for (let row = 0; row < ROWS; row++) {
103
- let line = pad;
104
- for (let li = 0; li < LETTERS.length; li++) {
105
- const letter = LETTERS[li];
106
- for (let col = 0; col < COLS_PER_LETTER; col++) {
107
- if (letter[row][col]) {
108
- line += chalk_1.default.hex(ROW_COLORS[row]).bold(BLOCK_ON);
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
- // Legacy named exports kept for compatibility with other modules
205
- exports.HELIX_LOGO_ASCII = LETTERS;
206
- exports.HELIX_BIG_ART = LETTERS;
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(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
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)('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
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
- const answer = await (0, prompts_1.default)({
183
- type: 'confirm',
184
- name: 'confirmed',
185
- message: '¿Permitir al agente ejecutar esta acción?',
186
- initial: true,
187
- });
188
- const reqId = req.request_id || req.id || '';
189
- if (answer.confirmed) {
190
- this.client.confirmAction(reqId);
191
- console.log((0, colors_1.GREEN)(' ✔ Acción confirmada.\n'));
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
- else {
194
- this.client.rejectAction(reqId);
195
- console.log((0, colors_1.RED)(' ✖ Acción rechazada.\n'));
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: true,
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
- await (0, apikey_1.handleConfigApiKey)(this.client);
264
- this.rl?.resume();
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
- await (0, skills_1.handleCreateSkill)(this.client);
275
- await this.autocompleter.refreshSuggestions();
276
- this.rl?.resume();
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
- await (0, mcp_1.handleCreateMcp)(this.client);
284
- await this.autocompleter.refreshSuggestions();
285
- this.rl?.resume();
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.1",
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 <prompt>')
41
- .description('Ejecutar un prompt directamente y esperar la respuesta')
42
- .action(async (promptText: string) => {
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
- // total character width: each cell = 2 chars
109
- const totalCols = LETTERS.length * COLS_PER_LETTER + (LETTERS.length - 1) * LETTER_GAP;
110
- const totalChars = totalCols * 2;
111
- const leftPad = Math.max(0, Math.floor((terminalWidth - totalChars) / 2));
112
- const pad = ' '.repeat(leftPad);
113
-
114
- const lines: string[] = [];
115
-
116
- for (let row = 0; row < ROWS; row++) {
117
- let line = pad;
118
-
119
- for (let li = 0; li < LETTERS.length; li++) {
120
- const letter = LETTERS[li];
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
- return chalk.bgRed.white.bold(' 🎙️ VOZ DETECTADA [HEY HELIX] 🔊 ');
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
- // Legacy named exports kept for compatibility with other modules
238
- export const HELIX_LOGO_ASCII = LETTERS;
239
- export const HELIX_BIG_ART = LETTERS;
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(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
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('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
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
- const answer = await prompts({
220
- type: 'confirm',
221
- name: 'confirmed',
222
- message: '¿Permitir al agente ejecutar esta acción?',
223
- initial: true,
224
- });
225
-
226
- const reqId = req.request_id || req.id || '';
227
- if (answer.confirmed) {
228
- this.client.confirmAction(reqId);
229
- console.log(GREEN(' ✔ Acción confirmada.\n'));
230
- } else {
231
- this.client.rejectAction(reqId);
232
- console.log(RED(' ✖ Acción rechazada.\n'));
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: true,
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
- await handleConfigApiKey(this.client);
313
- this.rl?.resume();
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
- await handleCreateSkill(this.client);
327
- await this.autocompleter.refreshSuggestions();
328
- this.rl?.resume();
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
- await handleCreateMcp(this.client);
338
- await this.autocompleter.refreshSuggestions();
339
- this.rl?.resume();
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
+ }