helix-assistant-cli 0.2.1 β†’ 0.2.7

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/ui/repl.js CHANGED
@@ -16,6 +16,7 @@ const skills_1 = require("../commands/skills");
16
16
  const mcp_1 = require("../commands/mcp");
17
17
  const mic_1 = require("../commands/mic");
18
18
  const browser_1 = require("../commands/browser");
19
+ const markdown_1 = require("./markdown");
19
20
  class HelixRepl {
20
21
  options;
21
22
  rl = null;
@@ -29,9 +30,11 @@ class HelixRepl {
29
30
  activeTaskId = null;
30
31
  activeTasksCount = 0;
31
32
  isProcessingPrompt = false;
33
+ plain = false;
32
34
  constructor(options) {
33
35
  this.options = options;
34
36
  this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
37
+ this.plain = Boolean(options.plain);
35
38
  this.client = new agent_client_1.AgentClient({
36
39
  wsUrl: options.wsUrl,
37
40
  wsToken: options.wsToken,
@@ -122,7 +125,7 @@ class HelixRepl {
122
125
  // Voice Capture indicator
123
126
  this.client.on('voice_capture_started', () => {
124
127
  this.isCapturingVoice = true;
125
- process.stdout.write(`\n${chalk_1.default.bgRed.white.bold(' πŸŽ™οΈ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
128
+ process.stdout.write(`\n${chalk_1.default.bgRed.white.bold(' πŸŽ™οΈ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
126
129
  });
127
130
  this.client.on('voice_capture_stopped', () => {
128
131
  this.isCapturingVoice = false;
@@ -150,8 +153,9 @@ class HelixRepl {
150
153
  // Branded header
151
154
  console.log('');
152
155
  console.log(` ${chalk_1.default.hex('#FF8C00').bold('⬑ HELIX')}${modelBadge}`);
153
- // Content with subtle left border
154
- const contentLines = content.trim().split('\n');
156
+ // Content with subtle left border and clean Markdown formatting
157
+ const formatted = (0, markdown_1.formatTerminalMarkdown)(content, this.plain);
158
+ const contentLines = formatted.trim().split('\n');
155
159
  for (const line of contentLines) {
156
160
  console.log(` ${chalk_1.default.gray('β”‚')} ${line}`);
157
161
  }
@@ -171,7 +175,7 @@ class HelixRepl {
171
175
  });
172
176
  }
173
177
  async _handleConfirmationDialog(req) {
174
- console.log(`\n${(0, colors_1.YELLOW_BOLD)('⚠️ CONFIRMACIΓ“N DE ACCIΓ“N REQUERIDA')}`);
178
+ console.log(`\n${(0, colors_1.YELLOW_BOLD)('⚠️ CONFIRMACIΓ“N DE ACCIΓ“N REQUERIDA')}`);
175
179
  console.log(` AcciΓ³n: ${chalk_1.default.white.bold(req.action || req.tool || 'AcciΓ³n del sistema')}`);
176
180
  if (req.description) {
177
181
  console.log(` DescripciΓ³n: ${(0, colors_1.DIM)(req.description)}`);
@@ -179,29 +183,45 @@ class HelixRepl {
179
183
  if (req.arguments && Object.keys(req.arguments).length > 0) {
180
184
  console.log(` ParΓ‘metros: ${(0, colors_1.GRAY)(JSON.stringify(req.arguments))}`);
181
185
  }
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'));
186
+ this.rl?.pause();
187
+ try {
188
+ const answer = await (0, prompts_1.default)({
189
+ type: 'confirm',
190
+ name: 'confirmed',
191
+ message: 'ΒΏPermitir al agente ejecutar esta acciΓ³n?',
192
+ initial: true,
193
+ });
194
+ const reqId = req.request_id || req.id || '';
195
+ if (answer && answer.confirmed) {
196
+ this.client.confirmAction(reqId);
197
+ console.log((0, colors_1.GREEN)(' βœ” AcciΓ³n confirmada.\n'));
198
+ }
199
+ else {
200
+ this.client.rejectAction(reqId);
201
+ console.log((0, colors_1.RED)(' βœ– AcciΓ³n rechazada.\n'));
202
+ }
192
203
  }
193
- else {
194
- this.client.rejectAction(reqId);
195
- console.log((0, colors_1.RED)(' βœ– AcciΓ³n rechazada.\n'));
204
+ finally {
205
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
206
+ try {
207
+ process.stdin.setRawMode(false);
208
+ }
209
+ catch { }
210
+ }
211
+ this.rl?.resume();
212
+ this._promptUser();
196
213
  }
197
- this._promptUser();
198
214
  }
199
215
  _startReadline() {
216
+ if (process.stdin.setEncoding) {
217
+ process.stdin.setEncoding('utf-8');
218
+ }
219
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
200
220
  const rl = readline_1.default.createInterface({
201
221
  input: process.stdin,
202
222
  output: process.stdout,
203
223
  completer: (line) => this.autocompleter.completer(line),
204
- terminal: true,
224
+ terminal: isInteractive,
205
225
  });
206
226
  this.rl = rl;
207
227
  rl.on('line', async (line) => {
@@ -260,8 +280,18 @@ class HelixRepl {
260
280
  break;
261
281
  case '/config-apikey':
262
282
  this.rl?.pause();
263
- await (0, apikey_1.handleConfigApiKey)(this.client);
264
- this.rl?.resume();
283
+ try {
284
+ await (0, apikey_1.handleConfigApiKey)(this.client);
285
+ }
286
+ finally {
287
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
288
+ try {
289
+ process.stdin.setRawMode(false);
290
+ }
291
+ catch { }
292
+ }
293
+ this.rl?.resume();
294
+ }
265
295
  break;
266
296
  case '/show-apikey':
267
297
  await (0, apikey_1.handleShowApiKey)(this.client);
@@ -271,18 +301,38 @@ class HelixRepl {
271
301
  break;
272
302
  case '/create-skill':
273
303
  this.rl?.pause();
274
- await (0, skills_1.handleCreateSkill)(this.client);
275
- await this.autocompleter.refreshSuggestions();
276
- this.rl?.resume();
304
+ try {
305
+ await (0, skills_1.handleCreateSkill)(this.client);
306
+ await this.autocompleter.refreshSuggestions();
307
+ }
308
+ finally {
309
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
310
+ try {
311
+ process.stdin.setRawMode(false);
312
+ }
313
+ catch { }
314
+ }
315
+ this.rl?.resume();
316
+ }
277
317
  break;
278
318
  case '/mcp':
279
319
  await (0, mcp_1.handleListMcp)(this.client);
280
320
  break;
281
321
  case '/create-mcp':
282
322
  this.rl?.pause();
283
- await (0, mcp_1.handleCreateMcp)(this.client);
284
- await this.autocompleter.refreshSuggestions();
285
- this.rl?.resume();
323
+ try {
324
+ await (0, mcp_1.handleCreateMcp)(this.client);
325
+ await this.autocompleter.refreshSuggestions();
326
+ }
327
+ finally {
328
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
329
+ try {
330
+ process.stdin.setRawMode(false);
331
+ }
332
+ catch { }
333
+ }
334
+ this.rl?.resume();
335
+ }
286
336
  break;
287
337
  case '/microphone': {
288
338
  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.7",
4
4
  "description": "HELIX - Interactive Terminal CLI for Windows AI Desktop Agent",
5
5
  "private": false,
6
6
  "publishConfig": {
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,19 +33,56 @@ 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
13
74
  .name('helix')
14
75
  .description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
15
- .version('0.1.0')
76
+ .version('0.2.7')
16
77
  .option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', getWsUrl())
17
78
  .option('-t, --token <token>', 'Token de autenticaciΓ³n WebSocket de HELIX', getWsToken())
79
+ .option('--plain', 'Desactivar formato enriquecido de Markdown y mostrar texto plano sin asteriscos')
18
80
  .action(async (options) => {
19
81
  // Default action: start interactive chat REPL
20
82
  const repl = new HelixRepl({
21
83
  wsUrl: options.wsUrl || getWsUrl(),
22
84
  wsToken: options.token || getWsToken(),
85
+ plain: Boolean(options.plain),
23
86
  });
24
87
  await repl.start();
25
88
  });
@@ -32,14 +95,23 @@ program
32
95
  const repl = new HelixRepl({
33
96
  wsUrl: opts.wsUrl || getWsUrl(),
34
97
  wsToken: opts.token || getWsToken(),
98
+ plain: Boolean(opts.plain),
35
99
  });
36
100
  await repl.start();
37
101
  });
38
102
 
39
103
  program
40
- .command('run <prompt>')
41
- .description('Ejecutar un prompt directamente y esperar la respuesta')
42
- .action(async (promptText: string) => {
104
+ .command('run [prompt...]')
105
+ .description('Ejecutar un prompt directamente y esperar la respuesta (soporta pipes: cat file | helix run)')
106
+ .action(async (promptParts: string[]) => {
107
+ let promptText = (promptParts || []).join(' ').trim();
108
+ if (!promptText || promptText === '-') {
109
+ promptText = await readStdin();
110
+ }
111
+ if (!promptText) {
112
+ console.error(RED('Error: Debes proporcionar un prompt como argumento o a travΓ©s de stdin (ej: echo "hola" | helix run).'));
113
+ process.exit(1);
114
+ }
43
115
  const opts = program.opts();
44
116
  const client = new AgentClient({
45
117
  wsUrl: opts.wsUrl || getWsUrl(),
@@ -47,6 +119,7 @@ program
47
119
  autoReconnect: false,
48
120
  });
49
121
 
122
+ console.log(DIM(`Prompt: ${promptText}\n`));
50
123
  console.log(CYAN(`Conectando con HELIX en ${client.url}...`));
51
124
  try {
52
125
  await client.connect();
@@ -55,7 +128,6 @@ program
55
128
  process.exit(1);
56
129
  }
57
130
 
58
- console.log(DIM(`Prompt: ${promptText}\n`));
59
131
  const convId = `cli_run_${Date.now()}`;
60
132
 
61
133
  client.on('react_step', (step: { iteration?: number }) => {
@@ -0,0 +1,45 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getSettingsFilePath } from '../config/env';
4
+
5
+ const SKILL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
6
+
7
+ /**
8
+ * Resolves the only user-controlled directory used by the CLI skill catalog.
9
+ * HELIX_SKILLS_DIR is explicit; otherwise it follows the agent's settings path.
10
+ */
11
+ export function getUserSkillsDirectory(): string {
12
+ const configured = process.env.HELIX_SKILLS_DIR?.trim();
13
+ if (configured) return path.resolve(configured);
14
+ return path.join(path.dirname(getSettingsFilePath()), 'skills');
15
+ }
16
+
17
+ /**
18
+ * Returns names only. The file contents are deliberately never read: skill
19
+ * instructions can contain secrets and are not needed for autocomplete.
20
+ * Discovery is shallow and rejects symlinked entries to avoid following paths
21
+ * outside the user's skills directory.
22
+ */
23
+ export function discoverUserSkillNames(skillsDirectory = getUserSkillsDirectory()): string[] {
24
+ const root = path.resolve(skillsDirectory);
25
+ let entries: fs.Dirent[];
26
+ try {
27
+ entries = fs.readdirSync(root, { withFileTypes: true });
28
+ } catch {
29
+ return [];
30
+ }
31
+
32
+ const names: string[] = [];
33
+ for (const entry of entries) {
34
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !SKILL_NAME.test(entry.name)) continue;
35
+
36
+ const skillFile = path.join(root, entry.name, 'SKILL.md');
37
+ try {
38
+ const file = fs.lstatSync(skillFile);
39
+ if (file.isFile() && !file.isSymbolicLink()) names.push(entry.name);
40
+ } catch {
41
+ // Ignore incomplete, inaccessible, or malformed user entries.
42
+ }
43
+ }
44
+ return names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
45
+ }
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import type { AgentClient } from '../client/agent-client';
3
3
  import { formatSkill, formatMcp, ORANGE, CYAN, MAGENTA, DIM } from './colors';
4
+ import { discoverUserSkillNames, getUserSkillsDirectory } from '../skills/user-skill-discovery';
4
5
 
5
6
  export interface CommandDefinition {
6
7
  command: string;
@@ -28,16 +29,29 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
28
29
 
29
30
  export class Autocompleter {
30
31
  private skills: string[] = [];
32
+ private remoteSkills: string[] = [];
33
+ private localSkills: string[] = [];
31
34
  private mcpTools: string[] = [];
35
+ private readonly userSkillsDirectory: string;
32
36
 
33
- constructor(private client?: AgentClient) {}
37
+ constructor(private client?: AgentClient, userSkillsDirectory = getUserSkillsDirectory()) {
38
+ this.userSkillsDirectory = userSkillsDirectory;
39
+ this.refreshLocalSkills();
40
+ }
41
+
42
+ private refreshLocalSkills(): void {
43
+ this.localSkills = discoverUserSkillNames(this.userSkillsDirectory);
44
+ const baseSkills = this.client ? this.remoteSkills : this.skills;
45
+ this.skills = [...new Set([...baseSkills, ...this.localSkills])]
46
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
47
+ }
34
48
 
35
49
  public async refreshSuggestions(): Promise<void> {
36
50
  if (!this.client || !this.client.connected) return;
37
51
  try {
38
52
  const skills = await this.client.getSkills();
39
53
  if (skills) {
40
- this.skills = skills.map((s) => s.name);
54
+ this.remoteSkills = skills.map((s) => s.name);
41
55
  }
42
56
  } catch {
43
57
  // Ignored if client is offline
@@ -60,6 +74,8 @@ export class Autocompleter {
60
74
  } catch {
61
75
  // Ignored
62
76
  }
77
+
78
+ this.refreshLocalSkills();
63
79
  }
64
80
 
65
81
  public getSkills(): string[] {
@@ -75,6 +91,9 @@ export class Autocompleter {
75
91
  * (line: string) => [string[], string]
76
92
  */
77
93
  public completer(line: string): [string[], string] {
94
+ // A skill may be created by another HELIX surface while the REPL is open.
95
+ // Keep Tab completion current without exposing the skill file contents.
96
+ this.refreshLocalSkills();
78
97
  const trimmed = line.trimStart();
79
98
 
80
99
  // 1. Skill sub-arguments for /skill <name>
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;