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.
Files changed (51) hide show
  1. package/bin/helix.js +3 -0
  2. package/dist/client/agent-client.d.ts +60 -0
  3. package/dist/client/agent-client.js +322 -0
  4. package/dist/client/agent-launcher.d.ts +30 -0
  5. package/dist/client/agent-launcher.js +339 -0
  6. package/dist/client/types.d.ts +88 -0
  7. package/dist/client/types.js +2 -0
  8. package/dist/commands/apikey.d.ts +3 -0
  9. package/dist/commands/apikey.js +123 -0
  10. package/dist/commands/browser.d.ts +2 -0
  11. package/dist/commands/browser.js +35 -0
  12. package/dist/commands/mcp.d.ts +3 -0
  13. package/dist/commands/mcp.js +102 -0
  14. package/dist/commands/mic.d.ts +2 -0
  15. package/dist/commands/mic.js +57 -0
  16. package/dist/commands/skills.d.ts +3 -0
  17. package/dist/commands/skills.js +105 -0
  18. package/dist/config/env.d.ts +6 -0
  19. package/dist/config/env.js +71 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +126 -0
  22. package/dist/ui/autocomplete.d.ts +25 -0
  23. package/dist/ui/autocomplete.js +142 -0
  24. package/dist/ui/banner.d.ts +24 -0
  25. package/dist/ui/banner.js +207 -0
  26. package/dist/ui/chat.d.ts +1 -0
  27. package/dist/ui/chat.js +5 -0
  28. package/dist/ui/colors.d.ts +34 -0
  29. package/dist/ui/colors.js +69 -0
  30. package/dist/ui/repl.d.ts +31 -0
  31. package/dist/ui/repl.js +402 -0
  32. package/package.json +31 -4
  33. package/src/client/agent-client.ts +387 -0
  34. package/src/client/agent-launcher.ts +374 -0
  35. package/src/client/types.ts +95 -0
  36. package/src/commands/apikey.ts +129 -0
  37. package/src/commands/browser.ts +39 -0
  38. package/src/commands/mcp.ts +105 -0
  39. package/src/commands/mic.ts +57 -0
  40. package/src/commands/skills.ts +109 -0
  41. package/src/config/env.ts +63 -0
  42. package/src/index.ts +139 -0
  43. package/src/ui/autocomplete.ts +153 -0
  44. package/src/ui/banner.ts +240 -0
  45. package/src/ui/chat.ts +1 -0
  46. package/src/ui/colors.ts +70 -0
  47. package/src/ui/repl.ts +473 -0
  48. package/tests/cli.test.ts +141 -0
  49. package/tsconfig.json +18 -0
  50. package/tsconfig.test.json +7 -0
  51. package/README.md +0 -3
@@ -0,0 +1,374 @@
1
+ import { spawn, ChildProcess, execSync } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import crypto from 'crypto';
5
+ import net from 'net';
6
+ import WebSocket from 'ws';
7
+
8
+ let spawnedAgentProcess: ChildProcess | null = null;
9
+ let activeSpawnToken: string | null = null;
10
+ let startupPromise: Promise<boolean> | null = null;
11
+
12
+ export function getTokenFilePath(): string {
13
+ const appData =
14
+ process.env.APPDATA ||
15
+ (process.platform === 'darwin'
16
+ ? path.join(process.env.HOME || '', 'Library/Application Support')
17
+ : path.join(process.env.HOME || '', '.config'));
18
+ const helixDir = path.join(appData, 'HELIX');
19
+ if (!fs.existsSync(helixDir)) {
20
+ try {
21
+ fs.mkdirSync(helixDir, { recursive: true });
22
+ } catch {}
23
+ }
24
+ return path.join(helixDir, '.ws_token');
25
+ }
26
+
27
+ export function getOrGenerateWsToken(): string {
28
+ if (activeSpawnToken) return activeSpawnToken;
29
+ if (process.env.HELIX_WS_TOKEN) {
30
+ activeSpawnToken = process.env.HELIX_WS_TOKEN;
31
+ return activeSpawnToken;
32
+ }
33
+
34
+ const tokenFile = getTokenFilePath();
35
+ if (fs.existsSync(tokenFile)) {
36
+ try {
37
+ const saved = fs.readFileSync(tokenFile, 'utf-8').trim();
38
+ if (saved.length >= 16) {
39
+ activeSpawnToken = saved;
40
+ process.env.HELIX_WS_TOKEN = activeSpawnToken;
41
+ return activeSpawnToken;
42
+ }
43
+ } catch {}
44
+ }
45
+
46
+ activeSpawnToken = crypto.randomBytes(32).toString('hex');
47
+ process.env.HELIX_WS_TOKEN = activeSpawnToken;
48
+ try {
49
+ fs.writeFileSync(tokenFile, activeSpawnToken, { encoding: 'utf-8', mode: 0o600 });
50
+ } catch {}
51
+ return activeSpawnToken;
52
+ }
53
+
54
+ export function isAgentProcessRunning(): boolean {
55
+ return spawnedAgentProcess !== null && !spawnedAgentProcess.killed;
56
+ }
57
+
58
+ export function isAgentStarting(): boolean {
59
+ return startupPromise !== null;
60
+ }
61
+
62
+ export function findPythonAgentScript(): string | null {
63
+ const candidates = [
64
+ path.resolve(process.cwd(), 'services/agent/main.py'),
65
+ path.resolve(__dirname, '../../../services/agent/main.py'),
66
+ path.resolve(__dirname, '../../../../services/agent/main.py'),
67
+ path.resolve(__dirname, '../../services/agent/main.py'),
68
+ ];
69
+
70
+ for (const candidate of candidates) {
71
+ if (fs.existsSync(candidate)) {
72
+ return candidate;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ export function findPythonExecutable(repoRoot: string): string {
79
+ // Check explicit environment variable first
80
+ if (process.env.PYTHON_PATH && fs.existsSync(process.env.PYTHON_PATH)) {
81
+ return process.env.PYTHON_PATH;
82
+ }
83
+
84
+ // On Windows, prefer pythonw.exe so NO console terminal window is opened
85
+ if (process.platform === 'win32') {
86
+ const venvGuiCandidates = [
87
+ path.join(repoRoot, '.venv', 'Scripts', 'pythonw.exe'),
88
+ path.join(repoRoot, 'venv', 'Scripts', 'pythonw.exe'),
89
+ path.join(repoRoot, 'services', 'agent', '.venv', 'Scripts', 'pythonw.exe'),
90
+ ];
91
+
92
+ for (const venvGui of venvGuiCandidates) {
93
+ if (fs.existsSync(venvGui)) {
94
+ return venvGui;
95
+ }
96
+ }
97
+ }
98
+
99
+ // Check virtual environment candidates relative to repository root
100
+ const venvCandidates = [
101
+ path.join(repoRoot, '.venv', 'Scripts', 'python.exe'),
102
+ path.join(repoRoot, '.venv', 'bin', 'python'),
103
+ path.join(repoRoot, 'venv', 'Scripts', 'python.exe'),
104
+ path.join(repoRoot, 'venv', 'bin', 'python'),
105
+ path.join(repoRoot, 'services', 'agent', '.venv', 'Scripts', 'python.exe'),
106
+ path.join(repoRoot, 'services', 'agent', '.venv', 'bin', 'python'),
107
+ ];
108
+
109
+ for (const venvPython of venvCandidates) {
110
+ if (fs.existsSync(venvPython)) {
111
+ return venvPython;
112
+ }
113
+ }
114
+
115
+ // Fallback to system PATH executables
116
+ const systemCandidates = process.platform === 'win32'
117
+ ? ['pythonw', 'python', 'py']
118
+ : ['python', 'python3', 'py'];
119
+ return systemCandidates[0];
120
+ }
121
+
122
+ /**
123
+ * Probes a TCP port quietly without initiating a WebSocket handshake.
124
+ */
125
+ export function probePort(port: number, host: string = '127.0.0.1', timeoutMs: number = 800): Promise<boolean> {
126
+ return new Promise((resolve) => {
127
+ const socket = new net.Socket();
128
+ let settled = false;
129
+
130
+ socket.setTimeout(timeoutMs);
131
+
132
+ socket.once('connect', () => {
133
+ if (!settled) {
134
+ settled = true;
135
+ socket.destroy();
136
+ resolve(true);
137
+ }
138
+ });
139
+
140
+ socket.once('timeout', () => {
141
+ if (!settled) {
142
+ settled = true;
143
+ socket.destroy();
144
+ resolve(false);
145
+ }
146
+ });
147
+
148
+ socket.once('error', () => {
149
+ if (!settled) {
150
+ settled = true;
151
+ socket.destroy();
152
+ resolve(false);
153
+ }
154
+ });
155
+
156
+ try {
157
+ socket.connect(port, host);
158
+ } catch {
159
+ if (!settled) {
160
+ settled = true;
161
+ resolve(false);
162
+ }
163
+ }
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Probes the WebSocket endpoint with Authorization header.
169
+ * Returns:
170
+ * 'ready': 101 Switching Protocols (authenticated and connected)
171
+ * 'unauthorized': 401 Unauthorized (wrong token / rogue process on port)
172
+ * 'closed': Connection refused / port closed
173
+ */
174
+ export function probeWebSocketAuth(
175
+ port: number = 8765,
176
+ token?: string,
177
+ host: string = '127.0.0.1',
178
+ timeoutMs: number = 1000
179
+ ): Promise<'ready' | 'unauthorized' | 'closed'> {
180
+ return new Promise((resolve) => {
181
+ let settled = false;
182
+ let timer: NodeJS.Timeout | null = null;
183
+
184
+ const headers: Record<string, string> = {};
185
+ const effectiveToken = token || getOrGenerateWsToken();
186
+ if (effectiveToken) {
187
+ headers['Authorization'] = `Bearer ${effectiveToken}`;
188
+ }
189
+
190
+ let ws: WebSocket | null = null;
191
+ try {
192
+ ws = new WebSocket(`ws://${host}:${port}`, { headers });
193
+ } catch {
194
+ return resolve('closed');
195
+ }
196
+
197
+ const cleanup = () => {
198
+ if (timer) clearTimeout(timer);
199
+ if (ws) {
200
+ ws.removeAllListeners();
201
+ ws.on('error', () => {});
202
+ try {
203
+ if (ws.readyState === WebSocket.OPEN) {
204
+ ws.close();
205
+ } else {
206
+ ws.terminate();
207
+ }
208
+ } catch {}
209
+ }
210
+ };
211
+
212
+ timer = setTimeout(() => {
213
+ if (!settled) {
214
+ settled = true;
215
+ cleanup();
216
+ resolve('closed');
217
+ }
218
+ }, timeoutMs);
219
+
220
+ ws.once('open', () => {
221
+ if (!settled) {
222
+ settled = true;
223
+ cleanup();
224
+ resolve('ready');
225
+ }
226
+ });
227
+
228
+ ws.once('error', (err: Error) => {
229
+ if (!settled) {
230
+ settled = true;
231
+ const msg = err.message || '';
232
+ cleanup();
233
+ if (msg.includes('401') || msg.includes('Unexpected server response: 401')) {
234
+ resolve('unauthorized');
235
+ } else {
236
+ resolve('closed');
237
+ }
238
+ }
239
+ });
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Terminates any rogue or orphaned process occupying the specified port.
245
+ */
246
+ export function killProcessOnPort(port: number = 8765): void {
247
+ try {
248
+ if (process.platform === 'win32') {
249
+ execSync(
250
+ `powershell -NoProfile -Command "$conn = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($conn) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }"`,
251
+ { stdio: 'ignore', windowsHide: true }
252
+ );
253
+ } else {
254
+ execSync(`fuser -k ${port}/tcp || lsof -ti tcp:${port} | xargs kill -9`, {
255
+ stdio: 'ignore',
256
+ });
257
+ }
258
+ } catch {
259
+ // Ignore errors
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Waits until the agent service has completed startup and successfully accepts WebSocket authentication.
265
+ */
266
+ export async function waitForAgentReady(
267
+ port: number = 8765,
268
+ token?: string,
269
+ maxWaitMs: number = 45000,
270
+ pollIntervalMs: number = 1000,
271
+ onProgress?: (elapsedSec: number) => void
272
+ ): Promise<boolean> {
273
+ if (startupPromise) {
274
+ return startupPromise;
275
+ }
276
+
277
+ startupPromise = (async () => {
278
+ const startTime = Date.now();
279
+ try {
280
+ while (Date.now() - startTime < maxWaitMs) {
281
+ // If the spawned child process exited prematurely with an error code, abort
282
+ if (spawnedAgentProcess !== null && spawnedAgentProcess.exitCode !== null) {
283
+ return false;
284
+ }
285
+
286
+ const authState = await probeWebSocketAuth(port, token);
287
+ if (authState === 'ready') {
288
+ return true;
289
+ }
290
+
291
+ const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
292
+ if (onProgress) {
293
+ onProgress(elapsedSec);
294
+ }
295
+
296
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
297
+ }
298
+ return false;
299
+ } finally {
300
+ startupPromise = null;
301
+ }
302
+ })();
303
+
304
+ return startupPromise;
305
+ }
306
+
307
+ export async function spawnAgentDaemon(port: number = 8765): Promise<{ success: boolean; error?: string }> {
308
+ const token = getOrGenerateWsToken();
309
+
310
+ // 1. Check if the agent is already up and authenticated
311
+ const currentStatus = await probeWebSocketAuth(port, token, '127.0.0.1', 500);
312
+ if (currentStatus === 'ready') {
313
+ return { success: true };
314
+ }
315
+
316
+ // 2. If port is occupied by an unauthorized process, clean it up
317
+ if (currentStatus === 'unauthorized') {
318
+ killProcessOnPort(port);
319
+ await new Promise((resolve) => setTimeout(resolve, 500));
320
+ }
321
+
322
+ if (isAgentProcessRunning() || isAgentStarting()) {
323
+ return { success: true };
324
+ }
325
+
326
+ const scriptPath = findPythonAgentScript();
327
+ if (!scriptPath) {
328
+ return {
329
+ success: false,
330
+ error: 'No se encontró el script services/agent/main.py del agente Python.',
331
+ };
332
+ }
333
+
334
+ const agentDir = path.dirname(scriptPath);
335
+ const repoRoot = path.resolve(agentDir, '../..');
336
+ const pythonExec = findPythonExecutable(repoRoot);
337
+
338
+ try {
339
+ const child = spawn(
340
+ pythonExec,
341
+ ['main.py', '--ws-port', String(port), '--no-camera'],
342
+ {
343
+ cwd: agentDir,
344
+ detached: true,
345
+ stdio: 'ignore', // run completely in background
346
+ windowsHide: true,
347
+ env: {
348
+ ...process.env,
349
+ HELIX_WS_TOKEN: token,
350
+ HELIX_DISABLE_CAMERA: '1',
351
+ CAMERA_ENABLED: 'false',
352
+ PYTHONUNBUFFERED: '1',
353
+ PYTHONIOENCODING: 'utf-8',
354
+ PYTHONPATH: repoRoot,
355
+ },
356
+ }
357
+ );
358
+
359
+ child.unref();
360
+ spawnedAgentProcess = child;
361
+
362
+ child.on('exit', () => {
363
+ spawnedAgentProcess = null;
364
+ });
365
+
366
+ return { success: true };
367
+ } catch (err) {
368
+ return {
369
+ success: false,
370
+ error: `Error al invocar proceso Python: ${(err as Error).message}`,
371
+ };
372
+ }
373
+ }
374
+
@@ -0,0 +1,95 @@
1
+ export interface AgentMessage<T = unknown> {
2
+ type: string;
3
+ payload: T;
4
+ timestamp?: string;
5
+ request_id?: string;
6
+ error?: string;
7
+ }
8
+
9
+ export interface UserTextMessagePayload {
10
+ text: string;
11
+ conversation_id: string;
12
+ conversation_history: Array<{ role: 'user' | 'assistant'; content: string }>;
13
+ attachments?: Array<{
14
+ name: string;
15
+ mime_type: string;
16
+ size?: number;
17
+ data_base64: string;
18
+ }>;
19
+ }
20
+
21
+ export interface SkillSummary {
22
+ name: string;
23
+ description: string;
24
+ triggers?: string[];
25
+ instructions?: string;
26
+ custom?: boolean;
27
+ }
28
+
29
+ export interface McpToolSummary {
30
+ name: string;
31
+ description?: string;
32
+ parameters?: Record<string, unknown>;
33
+ }
34
+
35
+ export interface McpServerConfig {
36
+ name: string;
37
+ command: string;
38
+ args?: string[];
39
+ env?: Record<string, string>;
40
+ status?: 'connected' | 'idle' | 'connecting' | 'error';
41
+ error?: string;
42
+ tools?: McpToolSummary[];
43
+ }
44
+
45
+ export interface ProviderKeySlot {
46
+ slot: number;
47
+ health: 'healthy' | 'degraded' | 'cooldown' | 'unconfigured' | 'error';
48
+ configured: boolean;
49
+ masked?: string;
50
+ }
51
+
52
+ export interface ProviderSummary {
53
+ id: string;
54
+ name: string;
55
+ configured: boolean;
56
+ keys: ProviderKeySlot[];
57
+ models_available: number;
58
+ }
59
+
60
+ export interface ModelSummary {
61
+ id: string;
62
+ name?: string;
63
+ provider: string;
64
+ capabilities?: Record<string, boolean>;
65
+ }
66
+
67
+ export interface TaskSummary {
68
+ task_id: string;
69
+ prompt?: string;
70
+ status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
71
+ created_at?: string;
72
+ result?: string;
73
+ error?: string;
74
+ }
75
+
76
+ export interface HelixSettings {
77
+ voice_enabled?: boolean;
78
+ wake_word?: string;
79
+ mcp_servers?: McpServerConfig[];
80
+ default_model?: string | null;
81
+ preferred_provider?: string | null;
82
+ log_level?: string;
83
+ agent_ws_port?: number;
84
+ [key: string]: unknown;
85
+ }
86
+
87
+ export interface ConfirmationRequest {
88
+ id?: string;
89
+ request_id?: string;
90
+ action?: string;
91
+ description?: string;
92
+ risk_level?: 'LOW' | 'MEDIUM' | 'HIGH';
93
+ tool?: string;
94
+ arguments?: Record<string, unknown>;
95
+ }
@@ -0,0 +1,129 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import type { AgentClient } from '../client/agent-client';
4
+ import { CYAN, GREEN, RED, YELLOW, GRAY, BOLD, DIM } from '../ui/colors';
5
+
6
+ const KNOWN_PROVIDERS = [
7
+ { title: 'OpenAI (GPT-4o, o1, o3-mini)', value: 'openai' },
8
+ { title: 'Anthropic (Claude 3.5 Sonnet, Claude 3.7)', value: 'anthropic' },
9
+ { title: 'Google (Gemini 2.0 Flash / Pro)', value: 'google' },
10
+ { title: 'Groq (Llama 3.3, DeepSeek R1)', value: 'groq' },
11
+ { title: 'Mistral AI (Mistral Large, Codestral)', value: 'mistral' },
12
+ { title: 'Ollama / Local Custom Provider', value: 'ollama' },
13
+ ];
14
+
15
+ export async function handleShowApiKey(client: AgentClient): Promise<void> {
16
+ console.log(`\n${BOLD('🔑 Configuración Actual de API Keys:')}\n`);
17
+
18
+ try {
19
+ const providers = await client.getProviders();
20
+ if (!providers || providers.length === 0) {
21
+ console.log(GRAY(' No se encontraron proveedores registrados en el agente.\n'));
22
+ return;
23
+ }
24
+
25
+ for (const provider of providers) {
26
+ const statusIcon = provider.configured ? GREEN('●') : GRAY('○');
27
+ const providerTitle = provider.configured
28
+ ? chalk.greenBright.bold(provider.name || provider.id)
29
+ : chalk.gray(provider.name || provider.id);
30
+
31
+ console.log(` ${statusIcon} ${providerTitle} ${DIM(`(${provider.models_available || 0} modelos disponibles)`)}`);
32
+
33
+ if (provider.keys && provider.keys.length > 0) {
34
+ for (const slot of provider.keys) {
35
+ let healthBadge: string;
36
+ switch (slot.health) {
37
+ case 'healthy':
38
+ healthBadge = chalk.green('✔ Válida / Activa');
39
+ break;
40
+ case 'degraded':
41
+ healthBadge = chalk.yellow('⚠ Degradada');
42
+ break;
43
+ case 'cooldown':
44
+ healthBadge = chalk.red('⏳ Cooldown');
45
+ break;
46
+ case 'unconfigured':
47
+ healthBadge = chalk.gray('○ No configurada');
48
+ break;
49
+ default:
50
+ healthBadge = chalk.gray(slot.health);
51
+ }
52
+
53
+ const maskedText = slot.configured ? chalk.cyan('••••••••••••••••') : chalk.gray('vacío');
54
+ console.log(` ${GRAY('Slot ' + slot.slot + ':')} ${maskedText} [${healthBadge}]`);
55
+ }
56
+ } else {
57
+ console.log(` ${GRAY('Sin slots de claves registrados')}`);
58
+ }
59
+ console.log('');
60
+ }
61
+ } catch (error) {
62
+ console.log(RED(` Error al consultar API keys del agente: ${(error as Error).message}\n`));
63
+ }
64
+ }
65
+
66
+ export async function handleConfigApiKey(client: AgentClient): Promise<void> {
67
+ console.log(`\n${BOLD('⚙️ Configurador Interactivo de API Keys')}\n`);
68
+
69
+ // 1. Select Provider
70
+ const providerAnswer = await prompts({
71
+ type: 'select',
72
+ name: 'provider',
73
+ message: 'Selecciona el proveedor de IA para configurar:',
74
+ choices: KNOWN_PROVIDERS,
75
+ });
76
+
77
+ const providerId = providerAnswer.provider as string | undefined;
78
+ if (!providerId) {
79
+ console.log(YELLOW('Operación cancelada.\n'));
80
+ return;
81
+ }
82
+
83
+ // 2. Select Slot
84
+ const slotAnswer = await prompts({
85
+ type: 'select',
86
+ name: 'slot',
87
+ message: `Selecciona el Slot para ${providerId.toUpperCase()}:`,
88
+ choices: [
89
+ { title: 'Slot 1 (Principal)', value: 1 },
90
+ { title: 'Slot 2 (Respaldo)', value: 2 },
91
+ { title: 'Slot 3 (Rotación)', value: 3 },
92
+ { title: 'Añadir nuevo Slot', value: 4 },
93
+ ],
94
+ });
95
+
96
+ const slotNumber = slotAnswer.slot as number | undefined;
97
+ if (!slotNumber) {
98
+ console.log(YELLOW('Operación cancelada.\n'));
99
+ return;
100
+ }
101
+
102
+ // 3. Prompt for API Key
103
+ const keyAnswer = await prompts({
104
+ type: 'password',
105
+ name: 'apiKey',
106
+ message: `Introduce la API Key para ${providerId.toUpperCase()} (Slot ${slotNumber}):`,
107
+ validate: (val: string) => (val && val.trim().length > 0 ? true : 'La API Key no puede estar vacía'),
108
+ });
109
+
110
+ const apiKey = (keyAnswer.apiKey as string | undefined)?.trim();
111
+ if (!apiKey) {
112
+ console.log(YELLOW('Operación cancelada.\n'));
113
+ return;
114
+ }
115
+
116
+ console.log(CYAN(`\n Validando clave con ${providerId}...`));
117
+
118
+ try {
119
+ const health = await client.validateKey(providerId, slotNumber, apiKey);
120
+ if (health === 'healthy') {
121
+ console.log(GREEN(`\n ✔ ¡API Key validada y guardada con éxito en el almacén seguro!`));
122
+ console.log(DIM(` Proveedor: ${providerId} | Slot: ${slotNumber} | Estado: healthy\n`));
123
+ } else {
124
+ console.log(YELLOW(`\n ⚠ La clave fue procesada pero su estado es: ${health}\n`));
125
+ }
126
+ } catch (err) {
127
+ console.log(RED(`\n ✖ Error al validar la API Key: ${(err as Error).message}\n`));
128
+ }
129
+ }
@@ -0,0 +1,39 @@
1
+ import chalk from 'chalk';
2
+ import type { AgentClient } from '../client/agent-client';
3
+ import { CYAN, GREEN, RED, YELLOW, GRAY, BOLD, DIM } from '../ui/colors';
4
+
5
+ export async function handleBrowserCommand(
6
+ client: AgentClient,
7
+ args?: string
8
+ ): Promise<void> {
9
+ const parts = args ? args.trim().split(/\s+/) : [];
10
+ const subCommand = parts[0]?.toLowerCase();
11
+ const targetUrl = parts[1];
12
+
13
+ if (subCommand === 'open' && targetUrl) {
14
+ console.log(CYAN(`\n Solicitando al agente abrir en navegador: ${targetUrl}...`));
15
+ client.sendUserText(
16
+ `Por favor abre la siguiente URL en el navegador: ${targetUrl}`,
17
+ `browser_${Date.now()}`
18
+ );
19
+ console.log(GREEN(` ✔ Comando enviado al agente.\n`));
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const settings = await client.getSettings();
25
+ const engine = (settings.browser_engine as string) || 'chromium';
26
+ const headless = settings.browser_headless !== false;
27
+
28
+ console.log(`\n${BOLD('🌐 Estado de Navegación y Browser Bridge:')}\n`);
29
+ console.log(` Motor: ${CYAN(engine.toUpperCase())}`);
30
+ console.log(` Modo: ${headless ? chalk.yellow('Headless') : chalk.green('Ventana Visible')}`);
31
+ console.log(` Descargas: ${settings.block_downloads ? RED('Bloqueadas') : GREEN('Permitidas')}`);
32
+ console.log(` Dominios: ${settings.block_untrusted_domains ? YELLOW('Filtrado Estricto') : GRAY('Abierto')}`);
33
+ console.log(`\n ${DIM('Comandos disponibles:')}`);
34
+ console.log(` ${CYAN('/browser')} ${DIM('- Ver estado de navegación')}`);
35
+ console.log(` ${CYAN('/browser open <url>')} ${DIM('- Abrir URL con el agente')}\n`);
36
+ } catch (err) {
37
+ console.log(RED(` Error al consultar estado del navegador: ${(err as Error).message}\n`));
38
+ }
39
+ }