helix-assistant-cli 0.2.14 → 0.6.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/README.md +25 -0
- package/dist/commands/browser.js +3 -2
- package/dist/commands/memory.d.ts +6 -0
- package/dist/commands/memory.js +129 -0
- package/dist/commands/model.d.ts +24 -0
- package/dist/commands/model.js +220 -0
- package/dist/commands/subagents.d.ts +15 -0
- package/dist/commands/subagents.js +123 -0
- package/dist/config/mcp-defaults.js +13 -4
- package/dist/index.js +5 -1
- package/dist/ui/autocomplete.js +17 -2
- package/dist/ui/browser-ascii.d.ts +2 -0
- package/dist/ui/browser-ascii.js +67 -0
- package/dist/ui/repl.d.ts +1 -0
- package/dist/ui/repl.js +48 -1
- package/package.json +3 -1
- package/src/commands/browser.ts +3 -2
- package/src/commands/memory.ts +167 -0
- package/src/commands/model.ts +237 -0
- package/src/commands/subagents.ts +150 -0
- package/src/config/mcp-defaults.ts +13 -4
- package/src/global.d.ts +23 -0
- package/src/index.ts +2 -1
- package/src/ui/autocomplete.ts +19 -2
- package/src/ui/browser-ascii.ts +82 -0
- package/src/ui/repl.ts +59 -1
- package/tests/cli.test.ts +41 -0
- package/tsconfig.json +3 -3
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { AgentClient } from '../client/agent-client';
|
|
2
|
+
import { CYAN_BOLD, DIM, RED, YELLOW } from '../ui/colors';
|
|
3
|
+
|
|
4
|
+
interface SubagentProfile {
|
|
5
|
+
profile_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SubagentStatus {
|
|
10
|
+
profile_id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
progress?: number;
|
|
14
|
+
current_action?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function boundedText(value: unknown, maxLength: number): string {
|
|
18
|
+
if (typeof value !== 'string') return '';
|
|
19
|
+
const clean = value.replace(/[\r\n|]/g, ' ').trim();
|
|
20
|
+
return clean.length > maxLength ? `${clean.slice(0, maxLength - 3)}...` : clean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function statusLabel(status?: string): string {
|
|
24
|
+
const normalized = status?.toLowerCase() || 'idle';
|
|
25
|
+
if (['running', 'active', 'in_progress'].includes(normalized)) return 'RUNNING';
|
|
26
|
+
if (['failed', 'error'].includes(normalized)) return 'FAILED';
|
|
27
|
+
if (['completed', 'done'].includes(normalized)) return 'DONE';
|
|
28
|
+
if (['cancelled', 'canceled'].includes(normalized)) return 'STOPPED';
|
|
29
|
+
return 'IDLE';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderSubagentPanel(
|
|
33
|
+
profiles: SubagentProfile[],
|
|
34
|
+
statuses: SubagentStatus[],
|
|
35
|
+
): string {
|
|
36
|
+
const statusByProfile = new Map<string, SubagentStatus>();
|
|
37
|
+
for (const status of statuses) {
|
|
38
|
+
if (status.profile_id) statusByProfile.set(status.profile_id, status);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rows = profiles.map((profile) => {
|
|
42
|
+
const status = statusByProfile.get(profile.profile_id);
|
|
43
|
+
const profileId = boundedText(profile.profile_id, 22);
|
|
44
|
+
const name = boundedText(profile.name, 18);
|
|
45
|
+
const state = statusLabel(status?.status);
|
|
46
|
+
const progress = Number.isFinite(status?.progress) ? ` ${status?.progress}%` : '';
|
|
47
|
+
const action = boundedText(status?.current_action, 26);
|
|
48
|
+
return [
|
|
49
|
+
profileId.padEnd(22),
|
|
50
|
+
name.padEnd(18),
|
|
51
|
+
`${state}${progress}`,
|
|
52
|
+
action,
|
|
53
|
+
];
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const headings = ['PROFILE ID', 'NAME', 'STATE', 'CURRENT ACTION'];
|
|
57
|
+
const widths = [22, 18, 16, 26];
|
|
58
|
+
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
|
|
59
|
+
const row = (values: string[]) =>
|
|
60
|
+
`| ${values.map((value, index) => value.padEnd(widths[index])).join(' | ')} |`;
|
|
61
|
+
const lines = [
|
|
62
|
+
border,
|
|
63
|
+
row(headings),
|
|
64
|
+
border,
|
|
65
|
+
...(rows.length > 0
|
|
66
|
+
? rows.map(row)
|
|
67
|
+
: [row(['No profiles', 'available', '', ''])]),
|
|
68
|
+
border,
|
|
69
|
+
];
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function asArray<T>(
|
|
74
|
+
value: unknown,
|
|
75
|
+
label: string,
|
|
76
|
+
isItem: (item: unknown) => item is T,
|
|
77
|
+
): T[] {
|
|
78
|
+
if (!Array.isArray(value) || !value.every(isItem)) {
|
|
79
|
+
throw new Error(`Agent returned an invalid ${label} list.`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSubagentProfile(value: unknown): value is SubagentProfile {
|
|
85
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
86
|
+
const profile = value as Record<string, unknown>;
|
|
87
|
+
return typeof profile.profile_id === 'string'
|
|
88
|
+
&& typeof profile.name === 'string';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isSubagentStatus(value: unknown): value is SubagentStatus {
|
|
92
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
93
|
+
const status = value as Record<string, unknown>;
|
|
94
|
+
return (status.profile_id === undefined || typeof status.profile_id === 'string')
|
|
95
|
+
&& (status.status === undefined || typeof status.status === 'string')
|
|
96
|
+
&& (status.progress === undefined || typeof status.progress === 'number')
|
|
97
|
+
&& (status.current_action === undefined || typeof status.current_action === 'string');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function handleSubagentsCommand(
|
|
101
|
+
client: AgentClient,
|
|
102
|
+
args = '',
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const trimmed = args.trim();
|
|
105
|
+
const chatMatch = trimmed.match(/^chat\s+(\S+)\s+([\s\S]+)$/i);
|
|
106
|
+
if (chatMatch) {
|
|
107
|
+
const [, profileId, message] = chatMatch;
|
|
108
|
+
try {
|
|
109
|
+
const result = await client.request<unknown>(
|
|
110
|
+
'CHAT_SUBAGENT',
|
|
111
|
+
{ profile_id: profileId, message: message.trim() },
|
|
112
|
+
120000,
|
|
113
|
+
);
|
|
114
|
+
console.log('\n' + CYAN_BOLD(` SUBAGENT ${profileId}`));
|
|
115
|
+
if (
|
|
116
|
+
result !== null
|
|
117
|
+
&& typeof result === 'object'
|
|
118
|
+
&& 'response' in result
|
|
119
|
+
&& typeof result.response === 'string'
|
|
120
|
+
) {
|
|
121
|
+
console.log(result.response);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(JSON.stringify(result, null, 2));
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log(RED(` No se pudo contactar al subagente: ${(error as Error).message}\n`));
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (trimmed && !/^list$/i.test(trimmed)) {
|
|
133
|
+
console.log(YELLOW(' Uso: /subagents [list] | /subagents chat <id> <mensaje>\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const [profilesValue, statusesValue] = await Promise.all([
|
|
139
|
+
client.request<unknown[]>('GET_SUBAGENT_PROFILES'),
|
|
140
|
+
client.request<unknown[]>('GET_SUBAGENT_STATUS'),
|
|
141
|
+
]);
|
|
142
|
+
const profiles = asArray(profilesValue, 'subagent profile', isSubagentProfile);
|
|
143
|
+
const statuses = asArray(statusesValue, 'subagent status', isSubagentStatus);
|
|
144
|
+
console.log(`\n${CYAN_BOLD(' HELIX SUBAGENTS')}`);
|
|
145
|
+
console.log(renderSubagentPanel(profiles, statuses));
|
|
146
|
+
console.log(DIM(' Chat: /subagents chat <profile_id> <mensaje>\n'));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.log(RED(` No se pudo cargar el panel de subagentes: ${(error as Error).message}\n`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -9,10 +9,19 @@ export interface McpDefaultDefinition {
|
|
|
9
9
|
|
|
10
10
|
/** Catalog only: entries are never enabled or installed automatically. */
|
|
11
11
|
export const MCP_DEFAULTS: McpDefaultDefinition[] = [
|
|
12
|
-
{ id: 'filesystem', label: 'Archivos locales', purpose: '
|
|
12
|
+
{ id: 'filesystem', label: 'Archivos locales', purpose: 'Carpetas y archivos del equipo del usuario', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo rutas autorizadas' },
|
|
13
13
|
{ id: 'obsidian', label: 'Obsidian / Helix Vault', purpose: 'Leer y gestionar una bóveda concreta', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo la bóveda seleccionada' },
|
|
14
14
|
{ id: 'vscode', label: 'VS Code', purpose: 'Trabajar con el workspace seleccionado', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo el workspace seleccionado' },
|
|
15
|
-
{ id: '
|
|
16
|
-
{ id: '
|
|
17
|
-
{ id: '
|
|
15
|
+
{ id: 'cursor', label: 'Cursor', purpose: 'Proyectos y editor de código del usuario', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo el workspace autorizado' },
|
|
16
|
+
{ id: 'github', label: 'GitHub', purpose: 'Repositorios, issues, pull requests y código', transport: 'stdio', activation: 'oauth-opt-in', privacy: 'Requiere token u OAuth del usuario' },
|
|
17
|
+
{ id: 'linear', label: 'Linear', purpose: 'Issues, roadmap y trabajo del equipo', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere acceso autorizado del workspace' },
|
|
18
|
+
{ id: 'gmail', label: 'Gmail', purpose: 'Correo, búsquedas y mensajes relevantes', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere OAuth explícito del usuario' },
|
|
19
|
+
{ id: 'google-drive', label: 'Google Drive', purpose: 'Documentos y archivos compartidos', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere acceso autorizado a la cuenta' },
|
|
20
|
+
{ id: 'notion', label: 'Notion', purpose: 'Páginas, bases de datos y conocimiento', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere integración autorizada' },
|
|
21
|
+
{ id: 'chatgpt', label: 'ChatGPT', purpose: 'Acceso a tareas y contexto de chat del usuario', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere conexión autorizada del usuario' },
|
|
22
|
+
{ id: 'claude-desktop', label: 'Claude Desktop', purpose: 'Contexto y trabajo en la app desktop de Claude', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo los recursos autorizados por el usuario' },
|
|
23
|
+
{ id: 'word', label: 'Microsoft Word', purpose: 'Documentos y edición de texto', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo archivos autorizados' },
|
|
24
|
+
{ id: 'excel', label: 'Microsoft Excel', purpose: 'Hojas de cálculo, análisis y tabulación', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo archivos autorizados' },
|
|
25
|
+
{ id: 'powerpoint', label: 'Microsoft PowerPoint', purpose: 'Presentaciones y material visual', transport: 'stdio', activation: 'local-opt-in', privacy: 'Solo archivos autorizados' },
|
|
26
|
+
{ id: 'antigravity', label: 'Antigravity', purpose: 'Workspace y flujos de automatización del usuario', transport: 'connector', activation: 'oauth-opt-in', privacy: 'Requiere conexión explícita del usuario' },
|
|
18
27
|
];
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Declaration shims for modules without bundled @types in this workspace.
|
|
2
|
+
// These are minimal stubs to satisfy TypeScript when explicit "types" is set.
|
|
3
|
+
|
|
4
|
+
declare module 'prompts' {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
function prompts(questions: any | any[]): Promise<Record<string, any>>;
|
|
7
|
+
export = prompts;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module 'jpeg-js' {
|
|
11
|
+
interface RawImageData {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
data: Buffer | Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
interface DecodeResult extends RawImageData {
|
|
17
|
+
comments?: string[];
|
|
18
|
+
}
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
function decode(jpegData: Buffer | Uint8Array, opts?: Record<string, any>): DecodeResult;
|
|
21
|
+
function encode(rawImageData: RawImageData, quality?: number): { data: Buffer; width: number; height: number };
|
|
22
|
+
export { decode, encode };
|
|
23
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { HelixSettings } from './client/types';
|
|
|
32
32
|
import { handleShowApiKey } from './commands/apikey';
|
|
33
33
|
import { ORANGE, CYAN, GREEN, RED, BOLD, DIM } from './ui/colors';
|
|
34
34
|
import { getAsciiLogo } from './ui/banner';
|
|
35
|
+
import packageJson from '../package.json';
|
|
35
36
|
|
|
36
37
|
async function readStdin(): Promise<string> {
|
|
37
38
|
if (process.stdin.isTTY) {
|
|
@@ -73,7 +74,7 @@ const program = new Command();
|
|
|
73
74
|
program
|
|
74
75
|
.name('helix')
|
|
75
76
|
.description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
|
|
76
|
-
.version(
|
|
77
|
+
.version(packageJson.version)
|
|
77
78
|
.option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', getWsUrl())
|
|
78
79
|
.option('-t, --token <token>', 'Token de autenticación WebSocket de HELIX', getWsToken())
|
|
79
80
|
.option('--plain', 'Desactivar formato enriquecido de Markdown y mostrar texto plano sin asteriscos')
|
package/src/ui/autocomplete.ts
CHANGED
|
@@ -14,12 +14,15 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
|
|
|
14
14
|
{ command: '/skill', description: 'Listar o inspeccionar skills registradas', usage: '/skill [nombre]' },
|
|
15
15
|
{ command: '/mcp', description: 'Listar servidores MCP y sus herramientas', usage: '/mcp [defaults]' },
|
|
16
16
|
{ command: '/create-mcp', description: 'Registrar y configurar un nuevo servidor MCP' },
|
|
17
|
-
{ command: '/browser', description: 'Ver estado
|
|
17
|
+
{ command: '/browser', description: 'Ver estado, abrir URLs o alternar la vista ASCII', usage: '/browser [open <url>|live [on|off]]' },
|
|
18
|
+
{ command: '/subagents', description: 'Ver el panel ASCII o chatear con un subagente', usage: '/subagents [list|chat <id> <mensaje>]' },
|
|
18
19
|
{ command: '/config-apikey', description: 'Configurador interactivo de API Keys (OpenAI, Anthropic, Gemini, etc.)' },
|
|
19
20
|
{ command: '/show-apikey', description: 'Ver estado y validez de las API Keys configuradas' },
|
|
20
21
|
{ command: '/microphone', description: 'Ver o alternar estado del micrófono', usage: '/microphone [enable|disable]' },
|
|
21
22
|
{ command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
|
|
23
|
+
{ command: '/model', description: 'Ver o cambiar el modelo activo (free/local/cloud)', usage: '/model [list|set <id>|effort <low|medium|high>]' },
|
|
22
24
|
{ command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
|
|
25
|
+
{ command: '/memory', description: 'Ver estadísticas del harness de aprendizaje adaptativo', usage: '/memory [stats|rules|reset]' },
|
|
23
26
|
{ command: '/status', description: 'Mostrar resumen del estado del sistema' },
|
|
24
27
|
{ command: '/providers', description: 'Ver proveedores configurados y salud de sus claves' },
|
|
25
28
|
{ command: '/capabilities', description: 'Ver capacidades activas: sandbox, subagentes, voz y browser' },
|
|
@@ -116,7 +119,21 @@ export class Autocompleter {
|
|
|
116
119
|
return [hits.length ? hits : options, line];
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// 3.
|
|
122
|
+
// 3. Model sub-arguments
|
|
123
|
+
if (trimmed.startsWith('/model ')) {
|
|
124
|
+
const options = ['/model list', '/model set ', '/model effort low', '/model effort medium', '/model effort high'];
|
|
125
|
+
const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
126
|
+
return [hits.length ? hits : options, line];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3b. Memory sub-arguments
|
|
130
|
+
if (trimmed.startsWith('/memory ')) {
|
|
131
|
+
const options = ['/memory stats', '/memory rules', '/memory reset'];
|
|
132
|
+
const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
133
|
+
return [hits.length ? hits : options, line];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 4. Browser sub-arguments
|
|
120
137
|
if (trimmed.startsWith('/browser ')) {
|
|
121
138
|
const options = ['/browser open https://'];
|
|
122
139
|
return [options, line];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import jpeg from 'jpeg-js';
|
|
2
|
+
|
|
3
|
+
const ASCII_RAMP = ' .:-=+*#%@';
|
|
4
|
+
const MAX_FRAME_WIDTH = 76;
|
|
5
|
+
const MAX_FRAME_HEIGHT = 34;
|
|
6
|
+
const MAX_JPEG_BYTES = 560_000;
|
|
7
|
+
|
|
8
|
+
export function grayscaleToAscii(
|
|
9
|
+
pixels: number[][],
|
|
10
|
+
width = MAX_FRAME_WIDTH,
|
|
11
|
+
): string {
|
|
12
|
+
if (!pixels.length || !pixels[0]?.length) {
|
|
13
|
+
throw new Error('Browser frame has no pixels to render.');
|
|
14
|
+
}
|
|
15
|
+
const boundedWidth = Math.max(1, Math.min(MAX_FRAME_WIDTH, Math.floor(width)));
|
|
16
|
+
const sourceHeight = pixels.length;
|
|
17
|
+
const sourceWidth = pixels[0].length;
|
|
18
|
+
const outputHeight = Math.max(
|
|
19
|
+
1,
|
|
20
|
+
Math.min(MAX_FRAME_HEIGHT, Math.round((sourceHeight / sourceWidth) * boundedWidth * 0.5)),
|
|
21
|
+
);
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (let y = 0; y < outputHeight; y += 1) {
|
|
25
|
+
const sourceY = Math.min(sourceHeight - 1, Math.floor((y / outputHeight) * sourceHeight));
|
|
26
|
+
let line = '';
|
|
27
|
+
for (let x = 0; x < boundedWidth; x += 1) {
|
|
28
|
+
const sourceX = Math.min(sourceWidth - 1, Math.floor((x / boundedWidth) * sourceWidth));
|
|
29
|
+
const value = Math.max(0, Math.min(255, pixels[sourceY][sourceX]));
|
|
30
|
+
const index = Math.round(((255 - value) / 255) * (ASCII_RAMP.length - 1));
|
|
31
|
+
line += ASCII_RAMP[index];
|
|
32
|
+
}
|
|
33
|
+
lines.push(line);
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderJpegAsAscii(base64: string, width = MAX_FRAME_WIDTH): string {
|
|
39
|
+
if (
|
|
40
|
+
typeof base64 !== 'string'
|
|
41
|
+
|| !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)
|
|
42
|
+
|| base64.length > Math.ceil(MAX_JPEG_BYTES * 4 / 3)
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('Browser frame is not a valid, bounded Base64 JPEG.');
|
|
45
|
+
}
|
|
46
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
47
|
+
if (bytes.length === 0 || bytes.length > MAX_JPEG_BYTES) {
|
|
48
|
+
throw new Error('Browser frame exceeds the supported JPEG size.');
|
|
49
|
+
}
|
|
50
|
+
const image = jpeg.decode(bytes, {
|
|
51
|
+
useTArray: true,
|
|
52
|
+
formatAsRGBA: true,
|
|
53
|
+
maxResolutionInMP: 4,
|
|
54
|
+
maxMemoryUsageInMB: 128,
|
|
55
|
+
});
|
|
56
|
+
const pixels: number[][] = [];
|
|
57
|
+
const outputWidth = Math.min(MAX_FRAME_WIDTH, image.width);
|
|
58
|
+
const outputHeight = Math.max(
|
|
59
|
+
1,
|
|
60
|
+
Math.min(
|
|
61
|
+
MAX_FRAME_HEIGHT,
|
|
62
|
+
Math.round((image.height / image.width) * outputWidth * 0.5),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (let y = 0; y < outputHeight; y += 1) {
|
|
67
|
+
const row: number[] = [];
|
|
68
|
+
const sourceY = Math.min(image.height - 1, Math.floor((y / outputHeight) * image.height));
|
|
69
|
+
for (let x = 0; x < outputWidth; x += 1) {
|
|
70
|
+
const sourceX = Math.min(image.width - 1, Math.floor((x / outputWidth) * image.width));
|
|
71
|
+
const offset = (sourceY * image.width + sourceX) * 4;
|
|
72
|
+
row.push(
|
|
73
|
+
image.data[offset] * 0.299
|
|
74
|
+
+ image.data[offset + 1] * 0.587
|
|
75
|
+
+ image.data[offset + 2] * 0.114,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
pixels.push(row);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return grayscaleToAscii(pixels, Math.min(width, outputWidth));
|
|
82
|
+
}
|
package/src/ui/repl.ts
CHANGED
|
@@ -24,9 +24,13 @@ import { handleCreateSkill, handleListSkills } from '../commands/skills';
|
|
|
24
24
|
import { handleCreateMcp, handleListMcp, handleMcpDefaults } from '../commands/mcp';
|
|
25
25
|
import { handleMicrophoneCommand } from '../commands/mic';
|
|
26
26
|
import { handleBrowserCommand } from '../commands/browser';
|
|
27
|
+
import { handleSubagentsCommand } from '../commands/subagents';
|
|
27
28
|
import type { ConfirmationRequest } from '../client/types';
|
|
28
29
|
import { formatTerminalMarkdown } from './markdown';
|
|
29
30
|
import { handleCapabilities, handleProviders, handleSettings } from '../commands/diagnostics';
|
|
31
|
+
import { renderJpegAsAscii } from './browser-ascii';
|
|
32
|
+
import { handleModelCommand } from '../commands/model';
|
|
33
|
+
import { handleMemoryCommand } from '../commands/memory';
|
|
30
34
|
|
|
31
35
|
export interface ReplOptions {
|
|
32
36
|
wsUrl: string;
|
|
@@ -47,6 +51,7 @@ export class HelixRepl {
|
|
|
47
51
|
private activeTasksCount: number = 0;
|
|
48
52
|
private isProcessingPrompt: boolean = false;
|
|
49
53
|
private plain: boolean = false;
|
|
54
|
+
private browserLiveViewEnabled: boolean = false;
|
|
50
55
|
|
|
51
56
|
constructor(private options: ReplOptions) {
|
|
52
57
|
this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
@@ -210,6 +215,29 @@ export class HelixRepl {
|
|
|
210
215
|
this.activeTaskId = task.task_id || null;
|
|
211
216
|
}
|
|
212
217
|
});
|
|
218
|
+
|
|
219
|
+
this.client.on('browser_frame', (payload: {
|
|
220
|
+
conversation_id?: string;
|
|
221
|
+
mime_type?: string;
|
|
222
|
+
image_base64?: string;
|
|
223
|
+
}) => {
|
|
224
|
+
if (
|
|
225
|
+
!this.browserLiveViewEnabled
|
|
226
|
+
|| !payload?.conversation_id?.startsWith('cli_')
|
|
227
|
+
|| payload.mime_type !== 'image/jpeg'
|
|
228
|
+
|| typeof payload.image_base64 !== 'string'
|
|
229
|
+
) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
process.stdout.write('\r\x1b[K');
|
|
234
|
+
console.log(`\n${CYAN_BOLD(' HELIX LIVE BROWSER')}`);
|
|
235
|
+
console.log(renderJpegAsAscii(payload.image_base64));
|
|
236
|
+
this._promptUser();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.log(RED(` No se pudo representar el cuadro del navegador: ${(error as Error).message}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
private async _handleConfirmationDialog(req: ConfirmationRequest): Promise<void> {
|
|
@@ -383,18 +411,48 @@ export class HelixRepl {
|
|
|
383
411
|
break;
|
|
384
412
|
}
|
|
385
413
|
|
|
386
|
-
case '/browser':
|
|
414
|
+
case '/browser': {
|
|
415
|
+
const browserArgs = args.trim().split(/\s+/);
|
|
416
|
+
if (browserArgs[0]?.toLowerCase() === 'live') {
|
|
417
|
+
const requestedState = browserArgs[1]?.toLowerCase();
|
|
418
|
+
if (requestedState && !['on', 'off'].includes(requestedState)) {
|
|
419
|
+
console.log(YELLOW(' Uso: /browser live [on|off]\n'));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
this.browserLiveViewEnabled = requestedState
|
|
423
|
+
? requestedState === 'on'
|
|
424
|
+
: !this.browserLiveViewEnabled;
|
|
425
|
+
console.log(
|
|
426
|
+
this.browserLiveViewEnabled
|
|
427
|
+
? GREEN(' Vista ASCII del navegador activada.\n')
|
|
428
|
+
: YELLOW(' Vista ASCII del navegador desactivada.\n'),
|
|
429
|
+
);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
387
432
|
await handleBrowserCommand(this.client, args);
|
|
388
433
|
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case '/subagents':
|
|
437
|
+
await handleSubagentsCommand(this.client, args);
|
|
438
|
+
break;
|
|
389
439
|
|
|
390
440
|
case '/tasks':
|
|
391
441
|
await this._handleListTasks();
|
|
392
442
|
break;
|
|
393
443
|
|
|
444
|
+
case '/model':
|
|
445
|
+
await handleModelCommand(this.client, args);
|
|
446
|
+
break;
|
|
447
|
+
|
|
394
448
|
case '/models':
|
|
395
449
|
await this._handleListModels();
|
|
396
450
|
break;
|
|
397
451
|
|
|
452
|
+
case '/memory':
|
|
453
|
+
await handleMemoryCommand(this.client, args);
|
|
454
|
+
break;
|
|
455
|
+
|
|
398
456
|
case '/status':
|
|
399
457
|
await this._handleStatus();
|
|
400
458
|
break;
|
package/tests/cli.test.ts
CHANGED
|
@@ -7,6 +7,9 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { discoverUserSkillNames } from '../src/skills/user-skill-discovery';
|
|
10
|
+
import { renderSubagentPanel } from '../src/commands/subagents';
|
|
11
|
+
import { grayscaleToAscii, renderJpegAsAscii } from '../src/ui/browser-ascii';
|
|
12
|
+
import jpeg from 'jpeg-js';
|
|
10
13
|
|
|
11
14
|
test('Autocompleter suggests slash commands', () => {
|
|
12
15
|
const completer = new Autocompleter();
|
|
@@ -18,6 +21,7 @@ test('Autocompleter suggests slash commands', () => {
|
|
|
18
21
|
assert.ok(completions.includes('/mcp'));
|
|
19
22
|
assert.ok(completions.includes('/create-mcp'));
|
|
20
23
|
assert.ok(completions.includes('/browser'));
|
|
24
|
+
assert.ok(completions.includes('/subagents'));
|
|
21
25
|
assert.ok(completions.includes('/config-apikey'));
|
|
22
26
|
assert.ok(completions.includes('/show-apikey'));
|
|
23
27
|
assert.ok(completions.includes('/microphone'));
|
|
@@ -28,6 +32,43 @@ test('Autocompleter suggests slash commands', () => {
|
|
|
28
32
|
assert.ok(completions.includes('/clear'));
|
|
29
33
|
});
|
|
30
34
|
|
|
35
|
+
test('Subagent panel renders ASCII rows with current status and action', () => {
|
|
36
|
+
const panel = renderSubagentPanel(
|
|
37
|
+
[{ profile_id: 'flowbot', name: 'Flowbot', role: 'Browser automation' }],
|
|
38
|
+
[{
|
|
39
|
+
profile_id: 'flowbot',
|
|
40
|
+
status: 'running',
|
|
41
|
+
progress: 50,
|
|
42
|
+
current_action: 'Opening browser',
|
|
43
|
+
}],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
assert.ok(panel.includes('+'));
|
|
47
|
+
assert.ok(panel.includes('flowbot'));
|
|
48
|
+
assert.ok(panel.includes('Flowbot'));
|
|
49
|
+
assert.ok(panel.includes('RUNNING'));
|
|
50
|
+
assert.ok(panel.includes('50%'));
|
|
51
|
+
assert.ok(panel.includes('Opening browser'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Browser image renderer maps grayscale to a bounded ASCII frame', () => {
|
|
55
|
+
assert.equal(
|
|
56
|
+
grayscaleToAscii([[0, 255], [0, 255], [128, 64], [128, 64]], 2),
|
|
57
|
+
'@ \n=#',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Browser image renderer decodes JPEG frames and rejects invalid payloads', () => {
|
|
62
|
+
const image = jpeg.encode({
|
|
63
|
+
data: Buffer.from([255, 255, 255, 255]),
|
|
64
|
+
width: 1,
|
|
65
|
+
height: 1,
|
|
66
|
+
}, 80);
|
|
67
|
+
|
|
68
|
+
assert.equal(renderJpegAsAscii(image.data.toString('base64'), 1), ' ');
|
|
69
|
+
assert.throws(() => renderJpegAsAscii('not-a-jpeg'), /valid, bounded Base64 JPEG/);
|
|
70
|
+
});
|
|
71
|
+
|
|
31
72
|
test('Autocompleter filters slash commands by prefix', () => {
|
|
32
73
|
const completer = new Autocompleter();
|
|
33
74
|
|
package/tsconfig.json
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"esModuleInterop": true,
|
|
10
10
|
"skipLibCheck": true,
|
|
11
11
|
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
// @ts-ignore - node is deprecated but required for CommonJS compatibility with current dependencies
|
|
13
12
|
"moduleResolution": "node",
|
|
14
13
|
"resolveJsonModule": true,
|
|
15
|
-
"declaration": true
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"types": ["node", "ws"]
|
|
16
16
|
},
|
|
17
17
|
"include": ["src/**/*"],
|
|
18
18
|
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|
|
19
|
+
}
|