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/index.js +75 -5
- package/dist/skills/user-skill-discovery.d.ts +12 -0
- package/dist/skills/user-skill-discovery.js +52 -0
- package/dist/ui/autocomplete.d.ts +5 -1
- package/dist/ui/autocomplete.js +18 -2
- package/dist/ui/banner.d.ts +3 -12
- package/dist/ui/banner.js +22 -145
- package/dist/ui/markdown.d.ts +5 -0
- package/dist/ui/markdown.js +49 -0
- package/dist/ui/repl.d.ts +2 -0
- package/dist/ui/repl.js +77 -27
- package/package.json +1 -1
- package/src/index.ts +77 -5
- package/src/skills/user-skill-discovery.ts +45 -0
- package/src/ui/autocomplete.ts +21 -2
- package/src/ui/banner.ts +22 -177
- package/src/ui/markdown.ts +51 -0
- package/src/ui/repl.ts +65 -29
- package/tests/cli.test.ts +106 -0
- package/tsconfig.json +2 -1
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(' ποΈ
|
|
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
|
|
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)('β οΈ
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
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.
|
|
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
|
|
41
|
-
.description('Ejecutar un prompt directamente y esperar la respuesta')
|
|
42
|
-
.action(async (
|
|
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
|
+
}
|
package/src/ui/autocomplete.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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;
|