helix-assistant-cli 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/helix.js +3 -0
- package/dist/client/agent-client.d.ts +60 -0
- package/dist/client/agent-client.js +322 -0
- package/dist/client/agent-launcher.d.ts +30 -0
- package/dist/client/agent-launcher.js +339 -0
- package/dist/client/types.d.ts +88 -0
- package/dist/client/types.js +2 -0
- package/dist/commands/apikey.d.ts +3 -0
- package/dist/commands/apikey.js +123 -0
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +102 -0
- package/dist/commands/mic.d.ts +2 -0
- package/dist/commands/mic.js +57 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +105 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +126 -0
- package/dist/ui/autocomplete.d.ts +25 -0
- package/dist/ui/autocomplete.js +142 -0
- package/dist/ui/banner.d.ts +24 -0
- package/dist/ui/banner.js +207 -0
- package/dist/ui/chat.d.ts +1 -0
- package/dist/ui/chat.js +5 -0
- package/dist/ui/colors.d.ts +34 -0
- package/dist/ui/colors.js +69 -0
- package/dist/ui/repl.d.ts +31 -0
- package/dist/ui/repl.js +402 -0
- package/package.json +31 -4
- package/src/client/agent-client.ts +387 -0
- package/src/client/agent-launcher.ts +374 -0
- package/src/client/types.ts +95 -0
- package/src/commands/apikey.ts +129 -0
- package/src/commands/browser.ts +39 -0
- package/src/commands/mcp.ts +105 -0
- package/src/commands/mic.ts +57 -0
- package/src/commands/skills.ts +109 -0
- package/src/config/env.ts +63 -0
- package/src/index.ts +139 -0
- package/src/ui/autocomplete.ts +153 -0
- package/src/ui/banner.ts +240 -0
- package/src/ui/chat.ts +1 -0
- package/src/ui/colors.ts +70 -0
- package/src/ui/repl.ts +473 -0
- package/tests/cli.test.ts +141 -0
- package/tsconfig.json +18 -0
- package/tsconfig.test.json +7 -0
- package/README.md +0 -3
package/src/ui/repl.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { AgentClient } from '../client/agent-client';
|
|
5
|
+
import { Autocompleter } from './autocomplete';
|
|
6
|
+
import { renderHeader, BannerStatusOptions } from './banner';
|
|
7
|
+
import {
|
|
8
|
+
ORANGE,
|
|
9
|
+
ORANGE_BOLD,
|
|
10
|
+
CYAN,
|
|
11
|
+
CYAN_BOLD,
|
|
12
|
+
GREEN,
|
|
13
|
+
RED,
|
|
14
|
+
RED_BOLD,
|
|
15
|
+
YELLOW,
|
|
16
|
+
YELLOW_BOLD,
|
|
17
|
+
GRAY,
|
|
18
|
+
BOLD,
|
|
19
|
+
DIM,
|
|
20
|
+
highlightMentions,
|
|
21
|
+
} from './colors';
|
|
22
|
+
import { handleConfigApiKey, handleShowApiKey } from '../commands/apikey';
|
|
23
|
+
import { handleCreateSkill, handleListSkills } from '../commands/skills';
|
|
24
|
+
import { handleCreateMcp, handleListMcp } from '../commands/mcp';
|
|
25
|
+
import { handleMicrophoneCommand } from '../commands/mic';
|
|
26
|
+
import { handleBrowserCommand } from '../commands/browser';
|
|
27
|
+
import type { ConfirmationRequest } from '../client/types';
|
|
28
|
+
|
|
29
|
+
export interface ReplOptions {
|
|
30
|
+
wsUrl: string;
|
|
31
|
+
wsToken?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class HelixRepl {
|
|
35
|
+
private rl: readline.Interface | null = null;
|
|
36
|
+
private client: AgentClient;
|
|
37
|
+
private autocompleter: Autocompleter;
|
|
38
|
+
private conversationId: string;
|
|
39
|
+
private conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> = [];
|
|
40
|
+
private voiceEnabled: boolean = true;
|
|
41
|
+
private isCapturingVoice: boolean = false;
|
|
42
|
+
private currentModel: string | null = null;
|
|
43
|
+
private activeTaskId: string | null = null;
|
|
44
|
+
private activeTasksCount: number = 0;
|
|
45
|
+
private isProcessingPrompt: boolean = false;
|
|
46
|
+
|
|
47
|
+
constructor(private options: ReplOptions) {
|
|
48
|
+
this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
49
|
+
this.client = new AgentClient({
|
|
50
|
+
wsUrl: options.wsUrl,
|
|
51
|
+
wsToken: options.wsToken,
|
|
52
|
+
});
|
|
53
|
+
this.autocompleter = new Autocompleter(this.client);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async start(): Promise<void> {
|
|
57
|
+
this._setupClientEvents();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await this.client.connect();
|
|
61
|
+
} catch {
|
|
62
|
+
// Connect in background if agent is still starting
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await this._syncSettings();
|
|
66
|
+
await this.autocompleter.refreshSuggestions();
|
|
67
|
+
|
|
68
|
+
// Render persistent initial banner
|
|
69
|
+
this.refreshBanner(true);
|
|
70
|
+
|
|
71
|
+
this._startReadline();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async _syncSettings(): Promise<void> {
|
|
75
|
+
if (!this.client.connected) return;
|
|
76
|
+
try {
|
|
77
|
+
// CLI mode: disable camera/gesture engine and skip per-tool verification
|
|
78
|
+
// (verification adds 2 extra LLM round-trips per tool call, not needed in CLI context)
|
|
79
|
+
await this.client.saveSettings({
|
|
80
|
+
camera_enabled: false,
|
|
81
|
+
tool_verification_enabled: false,
|
|
82
|
+
}).catch(() => {});
|
|
83
|
+
const settings = await this.client.getSettings();
|
|
84
|
+
if (typeof settings.voice_enabled === 'boolean') {
|
|
85
|
+
this.voiceEnabled = settings.voice_enabled;
|
|
86
|
+
}
|
|
87
|
+
if (typeof settings.default_model === 'string') {
|
|
88
|
+
this.currentModel = settings.default_model;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Fallback defaults
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public refreshBanner(clearScreen: boolean = false): void {
|
|
96
|
+
const status: BannerStatusOptions = {
|
|
97
|
+
connected: this.client.connected,
|
|
98
|
+
wsUrl: this.client.url,
|
|
99
|
+
voiceEnabled: this.voiceEnabled,
|
|
100
|
+
isCapturingVoice: this.isCapturingVoice,
|
|
101
|
+
currentModel: this.currentModel,
|
|
102
|
+
activeTasksCount: this.activeTasksCount,
|
|
103
|
+
};
|
|
104
|
+
renderHeader(status, clearScreen);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private waitingLogged: boolean = false;
|
|
108
|
+
|
|
109
|
+
private _setupClientEvents(): void {
|
|
110
|
+
this.client.on('connected', async () => {
|
|
111
|
+
this.waitingLogged = false;
|
|
112
|
+
await this._syncSettings();
|
|
113
|
+
await this.autocompleter.refreshSuggestions();
|
|
114
|
+
process.stdout.write(`\r\x1b[K${chalk.green('●')} ${chalk.greenBright.bold('Conectado al agente HELIX')} \n`);
|
|
115
|
+
this._promptUser();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.client.on('disconnected', () => {
|
|
119
|
+
if (this.client.isStarting) {
|
|
120
|
+
return; // Do not interrupt or print disconnect loops while agent is booting
|
|
121
|
+
}
|
|
122
|
+
if (!this.waitingLogged) {
|
|
123
|
+
this.waitingLogged = true;
|
|
124
|
+
console.log(`\n${RED('●')} ${chalk.yellowBright('Esperando al servicio del agente HELIX en ws://127.0.0.1:8765...')}`);
|
|
125
|
+
console.log(`${GRAY(' (Si no responde tras 3 reintentos, el CLI lo iniciará automáticamente en segundo plano)')}\n`);
|
|
126
|
+
}
|
|
127
|
+
this._promptUser();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.client.on('auto_spawning_agent', () => {
|
|
131
|
+
process.stdout.write(`\n${CYAN_BOLD('⚙️')} ${chalk.hex('#FF9F1C').bold('[AUTO-START]:')} Iniciando el servicio del agente HELIX en segundo plano...\n`);
|
|
132
|
+
process.stdout.write(`${GRAY(' Por favor espere mientras termina de inicializarse (no se realizarán reintentos hasta que esté listo).')}\n`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.client.on('agent_starting_progress', (data: { elapsedSec: number }) => {
|
|
136
|
+
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('⏳')} ${chalk.gray(`Inicializando subsistemas de HELIX en segundo plano... (${data.elapsedSec}s)`)}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.client.on('agent_ready', () => {
|
|
140
|
+
process.stdout.write(`\r\x1b[K ${chalk.green('✔')} ${chalk.greenBright('Servicio de HELIX listo. Conectando...')}\n\n`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.client.on('agent_start_failed', (data: { error?: string }) => {
|
|
144
|
+
process.stdout.write(`\r\x1b[K\n ${chalk.red('✖')} ${chalk.redBright('No se pudo iniciar el servicio automáticamente:')} ${chalk.gray(data?.error || 'Tiempo agotado')}\n`);
|
|
145
|
+
this._promptUser();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Voice Capture indicator
|
|
149
|
+
this.client.on('voice_capture_started', () => {
|
|
150
|
+
this.isCapturingVoice = true;
|
|
151
|
+
process.stdout.write(`\n${chalk.bgRed.white.bold(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.client.on('voice_capture_stopped', () => {
|
|
155
|
+
this.isCapturingVoice = false;
|
|
156
|
+
process.stdout.write(`\n${chalk.bgCyan.black.bold(' 🔊 [AUDIO RECIBIDO]: Procesando voz... ')}\n`);
|
|
157
|
+
this._promptUser();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ReAct Step Progress
|
|
161
|
+
this.client.on('react_step', (step: { iteration?: number; role?: string }) => {
|
|
162
|
+
const iter = step?.iteration ?? 1;
|
|
163
|
+
const role = step?.role ? chalk.gray(` (${step.role})`) : '';
|
|
164
|
+
process.stdout.write(`\r ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray(`Paso ${iter}${role} — Razonando...`)}`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Agent assistant message
|
|
168
|
+
this.client.on('agent_message', (payload: { role?: string; content?: string; model?: string; provider?: string }) => {
|
|
169
|
+
this.isProcessingPrompt = false;
|
|
170
|
+
this.activeTaskId = null;
|
|
171
|
+
if (this.activeTasksCount > 0) this.activeTasksCount--;
|
|
172
|
+
|
|
173
|
+
const content = payload?.content ?? '';
|
|
174
|
+
const modelBadge = payload?.model
|
|
175
|
+
? chalk.gray(` [${payload.model}]`)
|
|
176
|
+
: '';
|
|
177
|
+
|
|
178
|
+
// Clear any in-progress step line
|
|
179
|
+
process.stdout.write('\r\x1b[K');
|
|
180
|
+
|
|
181
|
+
// Branded header
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
|
|
184
|
+
|
|
185
|
+
// Content with subtle left border
|
|
186
|
+
const contentLines = content.trim().split('\n');
|
|
187
|
+
for (const line of contentLines) {
|
|
188
|
+
console.log(` ${chalk.gray('│')} ${line}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
this.conversationHistory.push({ role: 'assistant', content });
|
|
193
|
+
this._promptUser();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Confirmation request from agent
|
|
197
|
+
this.client.on('confirmation_request', async (req: ConfirmationRequest) => {
|
|
198
|
+
await this._handleConfirmationDialog(req);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Task updates
|
|
202
|
+
this.client.on('task_update', (task: { task_id?: string; status?: string; prompt?: string }) => {
|
|
203
|
+
if (task.status === 'RUNNING') {
|
|
204
|
+
this.activeTaskId = task.task_id || null;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async _handleConfirmationDialog(req: ConfirmationRequest): Promise<void> {
|
|
210
|
+
console.log(`\n${YELLOW_BOLD('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
|
|
211
|
+
console.log(` Acción: ${chalk.white.bold(req.action || req.tool || 'Acción del sistema')}`);
|
|
212
|
+
if (req.description) {
|
|
213
|
+
console.log(` Descripción: ${DIM(req.description)}`);
|
|
214
|
+
}
|
|
215
|
+
if (req.arguments && Object.keys(req.arguments).length > 0) {
|
|
216
|
+
console.log(` Parámetros: ${GRAY(JSON.stringify(req.arguments))}`);
|
|
217
|
+
}
|
|
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'));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this._promptUser();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private _startReadline(): void {
|
|
239
|
+
const rl = readline.createInterface({
|
|
240
|
+
input: process.stdin,
|
|
241
|
+
output: process.stdout,
|
|
242
|
+
completer: (line: string) => this.autocompleter.completer(line),
|
|
243
|
+
terminal: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.rl = rl;
|
|
247
|
+
|
|
248
|
+
rl.on('line', async (line: string) => {
|
|
249
|
+
await this._handleInputLine(line.trim());
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
rl.on('close', () => {
|
|
253
|
+
console.log(DIM('\nCerrando sesión de HELIX CLI...'));
|
|
254
|
+
this.client.close();
|
|
255
|
+
process.exit(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Ctrl+C handling: Cancel active task if running, or exit
|
|
259
|
+
rl.on('SIGINT', () => {
|
|
260
|
+
if (this.isProcessingPrompt && this.activeTaskId) {
|
|
261
|
+
console.log(YELLOW('\n[!] Cancelando tarea activa...'));
|
|
262
|
+
this.client.cancelTask(this.activeTaskId);
|
|
263
|
+
this.isProcessingPrompt = false;
|
|
264
|
+
this.activeTaskId = null;
|
|
265
|
+
this._promptUser();
|
|
266
|
+
} else {
|
|
267
|
+
console.log(DIM('\n¡Hasta pronto!'));
|
|
268
|
+
this.client.close();
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
this._promptUser();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _promptUser(): void {
|
|
277
|
+
if (!this.rl) return;
|
|
278
|
+
const promptPrefix = ` ${chalk.hex('#FF8C00').bold('helix')} ${chalk.gray('❯')} `;
|
|
279
|
+
this.rl.setPrompt(promptPrefix);
|
|
280
|
+
this.rl.prompt();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async _handleInputLine(line: string): Promise<void> {
|
|
284
|
+
if (!line) {
|
|
285
|
+
this._promptUser();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.client.isStarting) {
|
|
290
|
+
console.log(`\n ${chalk.yellow('⚠️')} ${chalk.yellowBright('El servicio de HELIX se está iniciando en segundo plano. Espera a que termine antes de enviar comandos o reintentar.')}\n`);
|
|
291
|
+
this._promptUser();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Slash commands handling ---
|
|
296
|
+
if (line.startsWith('/')) {
|
|
297
|
+
const parts = line.split(/\s+/);
|
|
298
|
+
const cmd = parts[0].toLowerCase();
|
|
299
|
+
const args = line.slice(cmd.length).trim();
|
|
300
|
+
|
|
301
|
+
switch (cmd) {
|
|
302
|
+
case '/help':
|
|
303
|
+
this.autocompleter.renderHelp();
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case '/clear':
|
|
307
|
+
this.refreshBanner(true);
|
|
308
|
+
break;
|
|
309
|
+
|
|
310
|
+
case '/config-apikey':
|
|
311
|
+
this.rl?.pause();
|
|
312
|
+
await handleConfigApiKey(this.client);
|
|
313
|
+
this.rl?.resume();
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case '/show-apikey':
|
|
317
|
+
await handleShowApiKey(this.client);
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case '/skill':
|
|
321
|
+
await handleListSkills(this.client, args || undefined);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case '/create-skill':
|
|
325
|
+
this.rl?.pause();
|
|
326
|
+
await handleCreateSkill(this.client);
|
|
327
|
+
await this.autocompleter.refreshSuggestions();
|
|
328
|
+
this.rl?.resume();
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case '/mcp':
|
|
332
|
+
await handleListMcp(this.client);
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case '/create-mcp':
|
|
336
|
+
this.rl?.pause();
|
|
337
|
+
await handleCreateMcp(this.client);
|
|
338
|
+
await this.autocompleter.refreshSuggestions();
|
|
339
|
+
this.rl?.resume();
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case '/microphone': {
|
|
343
|
+
const newState = await handleMicrophoneCommand(this.client, args, this.voiceEnabled);
|
|
344
|
+
this.voiceEnabled = newState;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case '/browser':
|
|
349
|
+
await handleBrowserCommand(this.client, args);
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case '/tasks':
|
|
353
|
+
await this._handleListTasks();
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case '/models':
|
|
357
|
+
await this._handleListModels();
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case '/status':
|
|
361
|
+
await this._handleStatus();
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case '/stop':
|
|
365
|
+
console.log(RED_BOLD('\n[PARADA DE EMERGENCIA]: Cancelando todas las tareas activas...'));
|
|
366
|
+
this.client.emergencyStop();
|
|
367
|
+
this.isProcessingPrompt = false;
|
|
368
|
+
this.activeTaskId = null;
|
|
369
|
+
console.log(GREEN('✔ Todas las tareas han sido detenidas.\n'));
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case '/exit':
|
|
373
|
+
case '/quit':
|
|
374
|
+
this.client.close();
|
|
375
|
+
process.exit(0);
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
default:
|
|
379
|
+
console.log(YELLOW(`Comando desconocido: "${cmd}". Escribe /help para ver la lista de comandos.`));
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._promptUser();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// --- Direct chat message / @skill / #mcp ---
|
|
388
|
+
if (!this.client.connected) {
|
|
389
|
+
console.log(YELLOW('⚠ El agente no está conectado actualmente. Intentando enviar de todos modos...'));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Highlight skills in strong orange (#FF8C00) and mcps in cyan
|
|
393
|
+
const knownSkills = this.autocompleter.getSkills();
|
|
394
|
+
const formattedEcho = highlightMentions(line, knownSkills);
|
|
395
|
+
|
|
396
|
+
// Record user message in history
|
|
397
|
+
this.conversationHistory.push({ role: 'user', content: line });
|
|
398
|
+
this.isProcessingPrompt = true;
|
|
399
|
+
this.activeTasksCount++;
|
|
400
|
+
|
|
401
|
+
process.stdout.write(` ${chalk.gray('→')} ${formattedEcho}\n`);
|
|
402
|
+
process.stdout.write(` ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray('Procesando...')}`);
|
|
403
|
+
|
|
404
|
+
const sent = this.client.sendUserText(
|
|
405
|
+
line,
|
|
406
|
+
this.conversationId,
|
|
407
|
+
this.conversationHistory.slice(-10)
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (!sent) {
|
|
411
|
+
process.stdout.write('\r\x1b[K');
|
|
412
|
+
console.log(RED('✖ Error: No se pudo enviar el mensaje (WebSocket desconectado).'));
|
|
413
|
+
this.isProcessingPrompt = false;
|
|
414
|
+
this.activeTasksCount = Math.max(0, this.activeTasksCount - 1);
|
|
415
|
+
this._promptUser();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async _handleListTasks(): Promise<void> {
|
|
420
|
+
try {
|
|
421
|
+
const tasks = await this.client.getTasks();
|
|
422
|
+
console.log(`\n${BOLD('📋 Tareas Registradas en HELIX:')}\n`);
|
|
423
|
+
if (!tasks || tasks.length === 0) {
|
|
424
|
+
console.log(GRAY(' No hay tareas recientes registradas.\n'));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const t of tasks.slice(-10)) {
|
|
429
|
+
let statusBadge = chalk.gray(t.status);
|
|
430
|
+
if (t.status === 'COMPLETED') statusBadge = chalk.green('✔ Completada');
|
|
431
|
+
if (t.status === 'RUNNING') statusBadge = chalk.yellow('⏳ En ejecución');
|
|
432
|
+
if (t.status === 'FAILED') statusBadge = chalk.red('✖ Fallida');
|
|
433
|
+
if (t.status === 'CANCELLED') statusBadge = chalk.gray('○ Cancelada');
|
|
434
|
+
|
|
435
|
+
console.log(` ${statusBadge} ${DIM(t.task_id)}`);
|
|
436
|
+
if (t.prompt) console.log(` ${chalk.white(t.prompt)}`);
|
|
437
|
+
console.log('');
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.log(RED(` Error al obtener tareas: ${(err as Error).message}\n`));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async _handleListModels(): Promise<void> {
|
|
445
|
+
try {
|
|
446
|
+
const models = await this.client.getModels();
|
|
447
|
+
console.log(`\n${BOLD('🧠 Modelos Disponibles en Proveedores Configurados:')}\n`);
|
|
448
|
+
if (!models || models.length === 0) {
|
|
449
|
+
console.log(GRAY(' No hay modelos disponibles. Configura tus API Keys con /config-apikey.\n'));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const m of models) {
|
|
454
|
+
console.log(` ${CYAN('•')} ${chalk.bold(m.id)} ${DIM(`[${m.provider}]`)}`);
|
|
455
|
+
}
|
|
456
|
+
console.log('');
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.log(RED(` Error al obtener modelos: ${(err as Error).message}\n`));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async _handleStatus(): Promise<void> {
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(` ${chalk.hex('#FF8C00').bold('⬡ Estado del Sistema')}`);
|
|
465
|
+
console.log(` ${chalk.gray('─'.repeat(40))}`);
|
|
466
|
+
console.log(` ${chalk.gray('Agente WS')} ${this.client.connected ? chalk.green('● Conectado') : chalk.red('● Desconectado')} ${chalk.gray(this.client.url)}`);
|
|
467
|
+
console.log(` ${chalk.gray('Micrófono')} ${this.voiceEnabled ? chalk.green('● Activo') + chalk.gray(' (hey helix)') : chalk.red('● Inactivo')}`);
|
|
468
|
+
console.log(` ${chalk.gray('Conversación')} ${chalk.gray(this.conversationId)} ${chalk.gray(`(${this.conversationHistory.length} msgs)`)}`);
|
|
469
|
+
console.log(` ${chalk.gray('Skills')} ${chalk.hex('#FF8C00').bold(this.autocompleter.getSkills().length.toString())} registradas`);
|
|
470
|
+
console.log(` ${chalk.gray('MCP Tools')} ${chalk.cyan.bold(this.autocompleter.getMcpTools().length.toString())} disponibles`);
|
|
471
|
+
console.log('');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Autocompleter, SLASH_COMMANDS } from '../src/ui/autocomplete';
|
|
4
|
+
import { formatSkill, formatMcp, highlightMentions, ORANGE } from '../src/ui/colors';
|
|
5
|
+
import { getAsciiLogo, getStatusBar, getVoiceIndicator } from '../src/ui/banner';
|
|
6
|
+
|
|
7
|
+
test('Autocompleter suggests slash commands', () => {
|
|
8
|
+
const completer = new Autocompleter();
|
|
9
|
+
|
|
10
|
+
// Test slash prefix
|
|
11
|
+
const [completions] = completer.completer('/');
|
|
12
|
+
assert.ok(completions.includes('/create-skill'));
|
|
13
|
+
assert.ok(completions.includes('/skill'));
|
|
14
|
+
assert.ok(completions.includes('/mcp'));
|
|
15
|
+
assert.ok(completions.includes('/create-mcp'));
|
|
16
|
+
assert.ok(completions.includes('/browser'));
|
|
17
|
+
assert.ok(completions.includes('/config-apikey'));
|
|
18
|
+
assert.ok(completions.includes('/show-apikey'));
|
|
19
|
+
assert.ok(completions.includes('/microphone'));
|
|
20
|
+
assert.ok(completions.includes('/help'));
|
|
21
|
+
assert.ok(completions.includes('/tasks'));
|
|
22
|
+
assert.ok(completions.includes('/models'));
|
|
23
|
+
assert.ok(completions.includes('/status'));
|
|
24
|
+
assert.ok(completions.includes('/clear'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('Autocompleter filters slash commands by prefix', () => {
|
|
28
|
+
const completer = new Autocompleter();
|
|
29
|
+
|
|
30
|
+
const [createMatches] = completer.completer('/create');
|
|
31
|
+
assert.ok(createMatches.includes('/create-skill'));
|
|
32
|
+
assert.ok(createMatches.includes('/create-mcp'));
|
|
33
|
+
assert.ok(!createMatches.includes('/microphone'));
|
|
34
|
+
|
|
35
|
+
const [micMatches] = completer.completer('/micro');
|
|
36
|
+
assert.deepEqual(micMatches, ['/microphone']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Autocompleter handles microphone sub-arguments', () => {
|
|
40
|
+
const completer = new Autocompleter();
|
|
41
|
+
|
|
42
|
+
const [micArgs] = completer.completer('/microphone ');
|
|
43
|
+
assert.ok(micArgs.includes('/microphone enable'));
|
|
44
|
+
assert.ok(micArgs.includes('/microphone disable'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Autocompleter handles @skills and #mcps autocompletion', () => {
|
|
48
|
+
const completer = new Autocompleter();
|
|
49
|
+
// Inject mock skills and mcp tools
|
|
50
|
+
(completer as any).skills = ['web-search', 'code-reviewer', 'email-assistant'];
|
|
51
|
+
(completer as any).mcpTools = ['filesystem_read_file', 'github_create_issue'];
|
|
52
|
+
|
|
53
|
+
// Test @ prefix
|
|
54
|
+
const [atMatches] = completer.completer('@');
|
|
55
|
+
assert.ok(atMatches.some((m: string) => m.includes('@web-search')));
|
|
56
|
+
assert.ok(atMatches.some((m: string) => m.includes('@code-reviewer')));
|
|
57
|
+
|
|
58
|
+
// Test @ partial query
|
|
59
|
+
const [codeMatches] = completer.completer('Please run @code');
|
|
60
|
+
assert.ok(codeMatches.some((m: string) => m.includes('@code-reviewer')));
|
|
61
|
+
|
|
62
|
+
// Test # prefix
|
|
63
|
+
const [hashMatches] = completer.completer('#');
|
|
64
|
+
assert.ok(hashMatches.some((m: string) => m.includes('#filesystem_read_file')));
|
|
65
|
+
|
|
66
|
+
// Test /skill sub-command
|
|
67
|
+
const [skillSub] = completer.completer('/skill web');
|
|
68
|
+
assert.deepEqual(skillSub, ['/skill web-search']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Colors: formatSkill applies strong orange formatting', () => {
|
|
72
|
+
const formatted = formatSkill('web-researcher');
|
|
73
|
+
assert.ok(formatted.includes('@web-researcher'));
|
|
74
|
+
// Verify it contains ANSI color escape sequence
|
|
75
|
+
assert.ok(formatted.includes('\u001b['));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('Colors: highlightMentions formats @skills and #mcps', () => {
|
|
79
|
+
const input = 'Use @summarize and #filesystem_read to check this report';
|
|
80
|
+
const highlighted = highlightMentions(input, ['summarize']);
|
|
81
|
+
|
|
82
|
+
assert.ok(highlighted.includes('@summarize'));
|
|
83
|
+
assert.ok(highlighted.includes('#filesystem_read'));
|
|
84
|
+
assert.ok(highlighted.includes('\u001b['));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Banner: getAsciiLogo contains HELIX art', () => {
|
|
88
|
+
const logo = getAsciiLogo();
|
|
89
|
+
assert.ok(logo.length > 20);
|
|
90
|
+
assert.ok(logo.includes('█') || logo.includes('ELIX'));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('Banner: getStatusBar reflects connection and mic state', () => {
|
|
94
|
+
const statusActive = getStatusBar({
|
|
95
|
+
connected: true,
|
|
96
|
+
wsUrl: 'ws://127.0.0.1:8765',
|
|
97
|
+
voiceEnabled: true,
|
|
98
|
+
isCapturingVoice: false,
|
|
99
|
+
currentModel: 'gpt-4o',
|
|
100
|
+
activeTasksCount: 0,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.ok(statusActive.includes('CONECTADO'));
|
|
104
|
+
assert.ok(statusActive.includes('hey helix'));
|
|
105
|
+
assert.ok(statusActive.includes('gpt-4o'));
|
|
106
|
+
|
|
107
|
+
const statusInactive = getStatusBar({
|
|
108
|
+
connected: false,
|
|
109
|
+
wsUrl: 'ws://127.0.0.1:8765',
|
|
110
|
+
voiceEnabled: false,
|
|
111
|
+
isCapturingVoice: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.ok(statusInactive.includes('DESCONECTADO'));
|
|
115
|
+
assert.ok(statusInactive.includes('Mic Off'));
|
|
116
|
+
|
|
117
|
+
const statusCapturing = getStatusBar({
|
|
118
|
+
connected: true,
|
|
119
|
+
wsUrl: 'ws://127.0.0.1:8765',
|
|
120
|
+
voiceEnabled: true,
|
|
121
|
+
isCapturingVoice: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
assert.ok(statusCapturing.includes('CAPTURANDO'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('Banner: getVoiceIndicator outputs proper badge', () => {
|
|
128
|
+
assert.ok(getVoiceIndicator(true, true).includes('HEY HELIX'));
|
|
129
|
+
assert.ok(getVoiceIndicator(false, true).includes('Mic Activo'));
|
|
130
|
+
assert.ok(getVoiceIndicator(false, false).includes('Mic Inactivo'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Launcher: probePort resolves false on closed random port', async () => {
|
|
134
|
+
const { probePort, isAgentStarting, findPythonExecutable } = await import('../src/client/agent-launcher');
|
|
135
|
+
// Port 59123 should be closed
|
|
136
|
+
const isListening = await probePort(59123, '127.0.0.1', 200);
|
|
137
|
+
assert.equal(isListening, false);
|
|
138
|
+
assert.equal(isAgentStarting(), false);
|
|
139
|
+
const pythonExec = findPythonExecutable(process.cwd());
|
|
140
|
+
assert.ok(pythonExec.length > 0);
|
|
141
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
package/README.md
DELETED