helix-assistant-cli 0.0.0-stage → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +193 -0
- package/dist/ui/autocomplete.d.ts +25 -0
- package/dist/ui/autocomplete.js +142 -0
- package/dist/ui/banner.d.ts +15 -0
- package/dist/ui/banner.js +84 -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 +448 -0
- package/package.json +30 -3
- 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 +208 -0
- package/src/ui/autocomplete.ts +153 -0
- package/src/ui/banner.ts +85 -0
- package/src/ui/chat.ts +1 -0
- package/src/ui/colors.ts +70 -0
- package/src/ui/repl.ts +504 -0
- package/tests/cli.test.ts +187 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +7 -0
- 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
|
+
}
|