helix-assistant-cli 0.2.4 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/agent-client.d.ts +1 -0
- package/dist/client/agent-client.js +3 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.js +47 -0
- package/dist/index.js +4 -1
- package/dist/skills/user-skill-discovery.d.ts +12 -0
- package/dist/skills/user-skill-discovery.js +52 -0
- package/dist/ui/autocomplete.d.ts +5 -1
- package/dist/ui/autocomplete.js +21 -2
- package/dist/ui/markdown.d.ts +5 -0
- package/dist/ui/markdown.js +49 -0
- package/dist/ui/repl.d.ts +2 -0
- package/dist/ui/repl.js +16 -2
- package/package.json +2 -2
- package/src/client/agent-client.ts +4 -0
- package/src/commands/diagnostics.ts +38 -0
- package/src/index.ts +4 -1
- package/src/skills/user-skill-discovery.ts +45 -0
- package/src/ui/autocomplete.ts +24 -2
- package/src/ui/markdown.ts +51 -0
- package/src/ui/repl.ts +20 -2
- package/tests/cli.test.ts +60 -0
|
@@ -42,6 +42,7 @@ export declare class AgentClient extends EventEmitter {
|
|
|
42
42
|
getProviders(): Promise<ProviderSummary[]>;
|
|
43
43
|
getModels(requirements?: Record<string, unknown>): Promise<ModelSummary[]>;
|
|
44
44
|
getSettings(): Promise<HelixSettings>;
|
|
45
|
+
getCapabilities(): Promise<Record<string, unknown>>;
|
|
45
46
|
saveSettings(settings: Partial<HelixSettings>): Promise<HelixSettings>;
|
|
46
47
|
validateKey(provider: string, slot: number, key: string): Promise<string>;
|
|
47
48
|
getSkills(): Promise<SkillSummary[]>;
|
|
@@ -265,6 +265,9 @@ class AgentClient extends events_1.EventEmitter {
|
|
|
265
265
|
async getSettings() {
|
|
266
266
|
return this.request('GET_SETTINGS', {});
|
|
267
267
|
}
|
|
268
|
+
async getCapabilities() {
|
|
269
|
+
return this.request('GET_CAPABILITIES', {});
|
|
270
|
+
}
|
|
268
271
|
async saveSettings(settings) {
|
|
269
272
|
return this.request('SAVE_SETTINGS', { settings });
|
|
270
273
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentClient } from '../client/agent-client';
|
|
2
|
+
export declare function handleProviders(client: AgentClient): Promise<void>;
|
|
3
|
+
export declare function handleCapabilities(client: AgentClient): Promise<void>;
|
|
4
|
+
export declare function handleSettings(client: AgentClient): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleProviders = handleProviders;
|
|
4
|
+
exports.handleCapabilities = handleCapabilities;
|
|
5
|
+
exports.handleSettings = handleSettings;
|
|
6
|
+
const colors_1 = require("../ui/colors");
|
|
7
|
+
async function handleProviders(client) {
|
|
8
|
+
try {
|
|
9
|
+
const providers = await client.getProviders();
|
|
10
|
+
console.log(`\n${(0, colors_1.BOLD)('🧠 Proveedores configurados:')}\n`);
|
|
11
|
+
for (const provider of providers) {
|
|
12
|
+
const state = provider.configured ? (0, colors_1.GREEN)('configurado') : (0, colors_1.GRAY)('sin clave');
|
|
13
|
+
console.log(` ${(0, colors_1.CYAN)(provider.name || provider.id)} [${state}] ${(0, colors_1.DIM)(`${provider.models_available} modelos`)}`);
|
|
14
|
+
for (const key of provider.keys || []) {
|
|
15
|
+
console.log(` ${(0, colors_1.DIM)(`slot ${key.slot}:`)} ${key.configured ? (0, colors_1.GREEN)(key.health) : (0, colors_1.GRAY)('vacío')} ${key.masked || ''}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
console.log('');
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.log((0, colors_1.RED)(` Error consultando proveedores: ${err.message}\n`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function handleCapabilities(client) {
|
|
25
|
+
try {
|
|
26
|
+
const caps = await client.getCapabilities();
|
|
27
|
+
console.log(`\n${(0, colors_1.BOLD)('⚙ Capacidades activas:')}\n`);
|
|
28
|
+
for (const [key, value] of Object.entries(caps)) {
|
|
29
|
+
const rendered = typeof value === 'boolean' ? (value ? (0, colors_1.GREEN)('activo') : (0, colors_1.YELLOW)('inactivo')) : (0, colors_1.CYAN)(String(value));
|
|
30
|
+
console.log(` ${(0, colors_1.DIM)(key.padEnd(24))} ${rendered}`);
|
|
31
|
+
}
|
|
32
|
+
console.log('');
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.log((0, colors_1.RED)(` Error consultando capacidades: ${err.message}\n`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function handleSettings(client) {
|
|
39
|
+
try {
|
|
40
|
+
const settings = await client.getSettings();
|
|
41
|
+
const safe = Object.fromEntries(Object.entries(settings).filter(([key]) => !/key|token|secret|password|auth/i.test(key)));
|
|
42
|
+
console.log(`\n${(0, colors_1.BOLD)('📋 Configuración efectiva (sin secretos):')}\n${JSON.stringify(safe, null, 2)}\n`);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.log((0, colors_1.RED)(` Error consultando configuración: ${err.message}\n`));
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -70,14 +70,16 @@ const program = new commander_1.Command();
|
|
|
70
70
|
program
|
|
71
71
|
.name('helix')
|
|
72
72
|
.description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
|
|
73
|
-
.version('0.
|
|
73
|
+
.version('0.2.7')
|
|
74
74
|
.option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', (0, env_1.getWsUrl)())
|
|
75
75
|
.option('-t, --token <token>', 'Token de autenticación WebSocket de HELIX', (0, env_1.getWsToken)())
|
|
76
|
+
.option('--plain', 'Desactivar formato enriquecido de Markdown y mostrar texto plano sin asteriscos')
|
|
76
77
|
.action(async (options) => {
|
|
77
78
|
// Default action: start interactive chat REPL
|
|
78
79
|
const repl = new repl_1.HelixRepl({
|
|
79
80
|
wsUrl: options.wsUrl || (0, env_1.getWsUrl)(),
|
|
80
81
|
wsToken: options.token || (0, env_1.getWsToken)(),
|
|
82
|
+
plain: Boolean(options.plain),
|
|
81
83
|
});
|
|
82
84
|
await repl.start();
|
|
83
85
|
});
|
|
@@ -89,6 +91,7 @@ program
|
|
|
89
91
|
const repl = new repl_1.HelixRepl({
|
|
90
92
|
wsUrl: opts.wsUrl || (0, env_1.getWsUrl)(),
|
|
91
93
|
wsToken: opts.token || (0, env_1.getWsToken)(),
|
|
94
|
+
plain: Boolean(opts.plain),
|
|
92
95
|
});
|
|
93
96
|
await repl.start();
|
|
94
97
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the only user-controlled directory used by the CLI skill catalog.
|
|
3
|
+
* HELIX_SKILLS_DIR is explicit; otherwise it follows the agent's settings path.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getUserSkillsDirectory(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Returns names only. The file contents are deliberately never read: skill
|
|
8
|
+
* instructions can contain secrets and are not needed for autocomplete.
|
|
9
|
+
* Discovery is shallow and rejects symlinked entries to avoid following paths
|
|
10
|
+
* outside the user's skills directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function discoverUserSkillNames(skillsDirectory?: string): string[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getUserSkillsDirectory = getUserSkillsDirectory;
|
|
7
|
+
exports.discoverUserSkillNames = discoverUserSkillNames;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const env_1 = require("../config/env");
|
|
11
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the only user-controlled directory used by the CLI skill catalog.
|
|
14
|
+
* HELIX_SKILLS_DIR is explicit; otherwise it follows the agent's settings path.
|
|
15
|
+
*/
|
|
16
|
+
function getUserSkillsDirectory() {
|
|
17
|
+
const configured = process.env.HELIX_SKILLS_DIR?.trim();
|
|
18
|
+
if (configured)
|
|
19
|
+
return path_1.default.resolve(configured);
|
|
20
|
+
return path_1.default.join(path_1.default.dirname((0, env_1.getSettingsFilePath)()), 'skills');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns names only. The file contents are deliberately never read: skill
|
|
24
|
+
* instructions can contain secrets and are not needed for autocomplete.
|
|
25
|
+
* Discovery is shallow and rejects symlinked entries to avoid following paths
|
|
26
|
+
* outside the user's skills directory.
|
|
27
|
+
*/
|
|
28
|
+
function discoverUserSkillNames(skillsDirectory = getUserSkillsDirectory()) {
|
|
29
|
+
const root = path_1.default.resolve(skillsDirectory);
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const names = [];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !SKILL_NAME.test(entry.name))
|
|
40
|
+
continue;
|
|
41
|
+
const skillFile = path_1.default.join(root, entry.name, 'SKILL.md');
|
|
42
|
+
try {
|
|
43
|
+
const file = fs_1.default.lstatSync(skillFile);
|
|
44
|
+
if (file.isFile() && !file.isSymbolicLink())
|
|
45
|
+
names.push(entry.name);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore incomplete, inaccessible, or malformed user entries.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
52
|
+
}
|
|
@@ -8,8 +8,12 @@ export declare const SLASH_COMMANDS: CommandDefinition[];
|
|
|
8
8
|
export declare class Autocompleter {
|
|
9
9
|
private client?;
|
|
10
10
|
private skills;
|
|
11
|
+
private remoteSkills;
|
|
12
|
+
private localSkills;
|
|
11
13
|
private mcpTools;
|
|
12
|
-
|
|
14
|
+
private readonly userSkillsDirectory;
|
|
15
|
+
constructor(client?: AgentClient | undefined, userSkillsDirectory?: string);
|
|
16
|
+
private refreshLocalSkills;
|
|
13
17
|
refreshSuggestions(): Promise<void>;
|
|
14
18
|
getSkills(): string[];
|
|
15
19
|
getMcpTools(): string[];
|
package/dist/ui/autocomplete.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Autocompleter = exports.SLASH_COMMANDS = void 0;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const colors_1 = require("./colors");
|
|
9
|
+
const user_skill_discovery_1 = require("../skills/user-skill-discovery");
|
|
9
10
|
exports.SLASH_COMMANDS = [
|
|
10
11
|
{ command: '/create-skill', description: 'Crear una nueva skill interactiva para HELIX' },
|
|
11
12
|
{ command: '/skill', description: 'Listar o inspeccionar skills registradas', usage: '/skill [nombre]' },
|
|
@@ -18,6 +19,9 @@ exports.SLASH_COMMANDS = [
|
|
|
18
19
|
{ command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
|
|
19
20
|
{ command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
|
|
20
21
|
{ command: '/status', description: 'Mostrar resumen del estado del sistema' },
|
|
22
|
+
{ command: '/providers', description: 'Ver proveedores configurados y salud de sus claves' },
|
|
23
|
+
{ command: '/capabilities', description: 'Ver capacidades activas: sandbox, subagentes, voz y browser' },
|
|
24
|
+
{ command: '/settings', description: 'Ver configuración efectiva sin exponer secretos' },
|
|
21
25
|
{ command: '/clear', description: 'Limpiar pantalla y restaurar el banner de HELIX' },
|
|
22
26
|
{ command: '/stop', description: 'Parada de emergencia de todas las tareas activas' },
|
|
23
27
|
{ command: '/help', description: 'Mostrar ayuda de comandos y atajos' },
|
|
@@ -26,9 +30,20 @@ exports.SLASH_COMMANDS = [
|
|
|
26
30
|
class Autocompleter {
|
|
27
31
|
client;
|
|
28
32
|
skills = [];
|
|
33
|
+
remoteSkills = [];
|
|
34
|
+
localSkills = [];
|
|
29
35
|
mcpTools = [];
|
|
30
|
-
|
|
36
|
+
userSkillsDirectory;
|
|
37
|
+
constructor(client, userSkillsDirectory = (0, user_skill_discovery_1.getUserSkillsDirectory)()) {
|
|
31
38
|
this.client = client;
|
|
39
|
+
this.userSkillsDirectory = userSkillsDirectory;
|
|
40
|
+
this.refreshLocalSkills();
|
|
41
|
+
}
|
|
42
|
+
refreshLocalSkills() {
|
|
43
|
+
this.localSkills = (0, user_skill_discovery_1.discoverUserSkillNames)(this.userSkillsDirectory);
|
|
44
|
+
const baseSkills = this.client ? this.remoteSkills : this.skills;
|
|
45
|
+
this.skills = [...new Set([...baseSkills, ...this.localSkills])]
|
|
46
|
+
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
32
47
|
}
|
|
33
48
|
async refreshSuggestions() {
|
|
34
49
|
if (!this.client || !this.client.connected)
|
|
@@ -36,7 +51,7 @@ class Autocompleter {
|
|
|
36
51
|
try {
|
|
37
52
|
const skills = await this.client.getSkills();
|
|
38
53
|
if (skills) {
|
|
39
|
-
this.
|
|
54
|
+
this.remoteSkills = skills.map((s) => s.name);
|
|
40
55
|
}
|
|
41
56
|
}
|
|
42
57
|
catch {
|
|
@@ -60,6 +75,7 @@ class Autocompleter {
|
|
|
60
75
|
catch {
|
|
61
76
|
// Ignored
|
|
62
77
|
}
|
|
78
|
+
this.refreshLocalSkills();
|
|
63
79
|
}
|
|
64
80
|
getSkills() {
|
|
65
81
|
return this.skills;
|
|
@@ -72,6 +88,9 @@ class Autocompleter {
|
|
|
72
88
|
* (line: string) => [string[], string]
|
|
73
89
|
*/
|
|
74
90
|
completer(line) {
|
|
91
|
+
// A skill may be created by another HELIX surface while the REPL is open.
|
|
92
|
+
// Keep Tab completion current without exposing the skill file contents.
|
|
93
|
+
this.refreshLocalSkills();
|
|
75
94
|
const trimmed = line.trimStart();
|
|
76
95
|
// 1. Skill sub-arguments for /skill <name>
|
|
77
96
|
if (trimmed.startsWith('/skill ')) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatTerminalMarkdown = formatTerminalMarkdown;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
/**
|
|
9
|
+
* Formats Markdown text for optimal display in terminal emulators,
|
|
10
|
+
* resolving visual noise from unparsed asterisks (*, **) and headers.
|
|
11
|
+
*/
|
|
12
|
+
function formatTerminalMarkdown(content, plain = false) {
|
|
13
|
+
if (!content)
|
|
14
|
+
return '';
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
const formattedLines = lines.map((line) => {
|
|
17
|
+
let formatted = line;
|
|
18
|
+
// Headers (#, ##, ###)
|
|
19
|
+
const headerMatch = formatted.match(/^(#{1,6})\s+(.*)$/);
|
|
20
|
+
if (headerMatch) {
|
|
21
|
+
const title = headerMatch[2].trim();
|
|
22
|
+
if (plain) {
|
|
23
|
+
return title.toUpperCase();
|
|
24
|
+
}
|
|
25
|
+
return chalk_1.default.hex('#FF9F1C').bold(title);
|
|
26
|
+
}
|
|
27
|
+
// Bullet lists (* item, - item)
|
|
28
|
+
if (/^\s*[*+-]\s+/.test(formatted)) {
|
|
29
|
+
formatted = formatted.replace(/^(\s*)[*+-]\s+/, (_m, indent) => {
|
|
30
|
+
return plain ? `${indent}- ` : `${indent}${chalk_1.default.hex('#FF8C00')('•')} `;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Inline bold (**text** or __text__)
|
|
34
|
+
formatted = formatted.replace(/(\*\*|__)(.*?)\1/g, (_m, _delim, text) => {
|
|
35
|
+
return plain ? text : chalk_1.default.bold(text);
|
|
36
|
+
});
|
|
37
|
+
// Inline italics (*text* or _text_)
|
|
38
|
+
formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)/g, (_m, t1, t2) => {
|
|
39
|
+
const text = t1 || t2;
|
|
40
|
+
return plain ? text : chalk_1.default.whiteBright(text);
|
|
41
|
+
});
|
|
42
|
+
// Inline code (`code`)
|
|
43
|
+
formatted = formatted.replace(/`([^`]+)`/g, (_m, code) => {
|
|
44
|
+
return plain ? code : chalk_1.default.cyan(code);
|
|
45
|
+
});
|
|
46
|
+
return formatted;
|
|
47
|
+
});
|
|
48
|
+
return formattedLines.join('\n');
|
|
49
|
+
}
|
package/dist/ui/repl.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface ReplOptions {
|
|
2
2
|
wsUrl: string;
|
|
3
3
|
wsToken?: string;
|
|
4
|
+
plain?: boolean;
|
|
4
5
|
}
|
|
5
6
|
export declare class HelixRepl {
|
|
6
7
|
private options;
|
|
@@ -15,6 +16,7 @@ export declare class HelixRepl {
|
|
|
15
16
|
private activeTaskId;
|
|
16
17
|
private activeTasksCount;
|
|
17
18
|
private isProcessingPrompt;
|
|
19
|
+
private plain;
|
|
18
20
|
constructor(options: ReplOptions);
|
|
19
21
|
start(): Promise<void>;
|
|
20
22
|
private _syncSettings;
|
package/dist/ui/repl.js
CHANGED
|
@@ -16,6 +16,8 @@ const skills_1 = require("../commands/skills");
|
|
|
16
16
|
const mcp_1 = require("../commands/mcp");
|
|
17
17
|
const mic_1 = require("../commands/mic");
|
|
18
18
|
const browser_1 = require("../commands/browser");
|
|
19
|
+
const markdown_1 = require("./markdown");
|
|
20
|
+
const diagnostics_1 = require("../commands/diagnostics");
|
|
19
21
|
class HelixRepl {
|
|
20
22
|
options;
|
|
21
23
|
rl = null;
|
|
@@ -29,9 +31,11 @@ class HelixRepl {
|
|
|
29
31
|
activeTaskId = null;
|
|
30
32
|
activeTasksCount = 0;
|
|
31
33
|
isProcessingPrompt = false;
|
|
34
|
+
plain = false;
|
|
32
35
|
constructor(options) {
|
|
33
36
|
this.options = options;
|
|
34
37
|
this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
38
|
+
this.plain = Boolean(options.plain);
|
|
35
39
|
this.client = new agent_client_1.AgentClient({
|
|
36
40
|
wsUrl: options.wsUrl,
|
|
37
41
|
wsToken: options.wsToken,
|
|
@@ -150,8 +154,9 @@ class HelixRepl {
|
|
|
150
154
|
// Branded header
|
|
151
155
|
console.log('');
|
|
152
156
|
console.log(` ${chalk_1.default.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
|
|
153
|
-
// Content with subtle left border
|
|
154
|
-
const
|
|
157
|
+
// Content with subtle left border and clean Markdown formatting
|
|
158
|
+
const formatted = (0, markdown_1.formatTerminalMarkdown)(content, this.plain);
|
|
159
|
+
const contentLines = formatted.trim().split('\n');
|
|
155
160
|
for (const line of contentLines) {
|
|
156
161
|
console.log(` ${chalk_1.default.gray('│')} ${line}`);
|
|
157
162
|
}
|
|
@@ -347,6 +352,15 @@ class HelixRepl {
|
|
|
347
352
|
case '/status':
|
|
348
353
|
await this._handleStatus();
|
|
349
354
|
break;
|
|
355
|
+
case '/providers':
|
|
356
|
+
await (0, diagnostics_1.handleProviders)(this.client);
|
|
357
|
+
break;
|
|
358
|
+
case '/capabilities':
|
|
359
|
+
await (0, diagnostics_1.handleCapabilities)(this.client);
|
|
360
|
+
break;
|
|
361
|
+
case '/settings':
|
|
362
|
+
await (0, diagnostics_1.handleSettings)(this.client);
|
|
363
|
+
break;
|
|
350
364
|
case '/stop':
|
|
351
365
|
console.log((0, colors_1.RED_BOLD)('\n[PARADA DE EMERGENCIA]: Cancelando todas las tareas activas...'));
|
|
352
366
|
this.client.emergencyStop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helix-assistant-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "HELIX - Interactive Terminal CLI for Windows AI Desktop Agent",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"@types/ws": "^8.5.10",
|
|
31
31
|
"typescript": "^5.3.0"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
@@ -319,6 +319,10 @@ export class AgentClient extends EventEmitter {
|
|
|
319
319
|
return this.request<HelixSettings>('GET_SETTINGS', {});
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
public async getCapabilities(): Promise<Record<string, unknown>> {
|
|
323
|
+
return this.request<Record<string, unknown>>('GET_CAPABILITIES', {});
|
|
324
|
+
}
|
|
325
|
+
|
|
322
326
|
public async saveSettings(settings: Partial<HelixSettings>): Promise<HelixSettings> {
|
|
323
327
|
return this.request<HelixSettings>('SAVE_SETTINGS', { settings });
|
|
324
328
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import type { AgentClient } from '../client/agent-client';
|
|
3
|
+
import { BOLD, CYAN, DIM, GREEN, GRAY, RED, YELLOW } from '../ui/colors';
|
|
4
|
+
|
|
5
|
+
export async function handleProviders(client: AgentClient): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
const providers = await client.getProviders();
|
|
8
|
+
console.log(`\n${BOLD('🧠 Proveedores configurados:')}\n`);
|
|
9
|
+
for (const provider of providers) {
|
|
10
|
+
const state = provider.configured ? GREEN('configurado') : GRAY('sin clave');
|
|
11
|
+
console.log(` ${CYAN(provider.name || provider.id)} [${state}] ${DIM(`${provider.models_available} modelos`)}`);
|
|
12
|
+
for (const key of provider.keys || []) {
|
|
13
|
+
console.log(` ${DIM(`slot ${key.slot}:`)} ${key.configured ? GREEN(key.health) : GRAY('vacío')} ${key.masked || ''}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
console.log('');
|
|
17
|
+
} catch (err) { console.log(RED(` Error consultando proveedores: ${(err as Error).message}\n`)); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleCapabilities(client: AgentClient): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const caps = await client.getCapabilities();
|
|
23
|
+
console.log(`\n${BOLD('⚙ Capacidades activas:')}\n`);
|
|
24
|
+
for (const [key, value] of Object.entries(caps)) {
|
|
25
|
+
const rendered = typeof value === 'boolean' ? (value ? GREEN('activo') : YELLOW('inactivo')) : CYAN(String(value));
|
|
26
|
+
console.log(` ${DIM(key.padEnd(24))} ${rendered}`);
|
|
27
|
+
}
|
|
28
|
+
console.log('');
|
|
29
|
+
} catch (err) { console.log(RED(` Error consultando capacidades: ${(err as Error).message}\n`)); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handleSettings(client: AgentClient): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const settings = await client.getSettings();
|
|
35
|
+
const safe = Object.fromEntries(Object.entries(settings).filter(([key]) => !/key|token|secret|password|auth/i.test(key)));
|
|
36
|
+
console.log(`\n${BOLD('📋 Configuración efectiva (sin secretos):')}\n${JSON.stringify(safe, null, 2)}\n`);
|
|
37
|
+
} catch (err) { console.log(RED(` Error consultando configuración: ${(err as Error).message}\n`)); }
|
|
38
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -73,14 +73,16 @@ const program = new Command();
|
|
|
73
73
|
program
|
|
74
74
|
.name('helix')
|
|
75
75
|
.description('HELIX - Interactive Terminal CLI for Windows AI Desktop Agent')
|
|
76
|
-
.version('0.
|
|
76
|
+
.version('0.2.7')
|
|
77
77
|
.option('-u, --ws-url <url>', 'URL del servidor WebSocket del agente', getWsUrl())
|
|
78
78
|
.option('-t, --token <token>', 'Token de autenticación WebSocket de HELIX', getWsToken())
|
|
79
|
+
.option('--plain', 'Desactivar formato enriquecido de Markdown y mostrar texto plano sin asteriscos')
|
|
79
80
|
.action(async (options) => {
|
|
80
81
|
// Default action: start interactive chat REPL
|
|
81
82
|
const repl = new HelixRepl({
|
|
82
83
|
wsUrl: options.wsUrl || getWsUrl(),
|
|
83
84
|
wsToken: options.token || getWsToken(),
|
|
85
|
+
plain: Boolean(options.plain),
|
|
84
86
|
});
|
|
85
87
|
await repl.start();
|
|
86
88
|
});
|
|
@@ -93,6 +95,7 @@ program
|
|
|
93
95
|
const repl = new HelixRepl({
|
|
94
96
|
wsUrl: opts.wsUrl || getWsUrl(),
|
|
95
97
|
wsToken: opts.token || getWsToken(),
|
|
98
|
+
plain: Boolean(opts.plain),
|
|
96
99
|
});
|
|
97
100
|
await repl.start();
|
|
98
101
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getSettingsFilePath } from '../config/env';
|
|
4
|
+
|
|
5
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the only user-controlled directory used by the CLI skill catalog.
|
|
9
|
+
* HELIX_SKILLS_DIR is explicit; otherwise it follows the agent's settings path.
|
|
10
|
+
*/
|
|
11
|
+
export function getUserSkillsDirectory(): string {
|
|
12
|
+
const configured = process.env.HELIX_SKILLS_DIR?.trim();
|
|
13
|
+
if (configured) return path.resolve(configured);
|
|
14
|
+
return path.join(path.dirname(getSettingsFilePath()), 'skills');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns names only. The file contents are deliberately never read: skill
|
|
19
|
+
* instructions can contain secrets and are not needed for autocomplete.
|
|
20
|
+
* Discovery is shallow and rejects symlinked entries to avoid following paths
|
|
21
|
+
* outside the user's skills directory.
|
|
22
|
+
*/
|
|
23
|
+
export function discoverUserSkillNames(skillsDirectory = getUserSkillsDirectory()): string[] {
|
|
24
|
+
const root = path.resolve(skillsDirectory);
|
|
25
|
+
let entries: fs.Dirent[];
|
|
26
|
+
try {
|
|
27
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const names: string[] = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !SKILL_NAME.test(entry.name)) continue;
|
|
35
|
+
|
|
36
|
+
const skillFile = path.join(root, entry.name, 'SKILL.md');
|
|
37
|
+
try {
|
|
38
|
+
const file = fs.lstatSync(skillFile);
|
|
39
|
+
if (file.isFile() && !file.isSymbolicLink()) names.push(entry.name);
|
|
40
|
+
} catch {
|
|
41
|
+
// Ignore incomplete, inaccessible, or malformed user entries.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
45
|
+
}
|
package/src/ui/autocomplete.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import type { AgentClient } from '../client/agent-client';
|
|
3
3
|
import { formatSkill, formatMcp, ORANGE, CYAN, MAGENTA, DIM } from './colors';
|
|
4
|
+
import { discoverUserSkillNames, getUserSkillsDirectory } from '../skills/user-skill-discovery';
|
|
4
5
|
|
|
5
6
|
export interface CommandDefinition {
|
|
6
7
|
command: string;
|
|
@@ -20,6 +21,9 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
|
|
|
20
21
|
{ command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
|
|
21
22
|
{ command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
|
|
22
23
|
{ command: '/status', description: 'Mostrar resumen del estado del sistema' },
|
|
24
|
+
{ command: '/providers', description: 'Ver proveedores configurados y salud de sus claves' },
|
|
25
|
+
{ command: '/capabilities', description: 'Ver capacidades activas: sandbox, subagentes, voz y browser' },
|
|
26
|
+
{ command: '/settings', description: 'Ver configuración efectiva sin exponer secretos' },
|
|
23
27
|
{ command: '/clear', description: 'Limpiar pantalla y restaurar el banner de HELIX' },
|
|
24
28
|
{ command: '/stop', description: 'Parada de emergencia de todas las tareas activas' },
|
|
25
29
|
{ command: '/help', description: 'Mostrar ayuda de comandos y atajos' },
|
|
@@ -28,16 +32,29 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
|
|
|
28
32
|
|
|
29
33
|
export class Autocompleter {
|
|
30
34
|
private skills: string[] = [];
|
|
35
|
+
private remoteSkills: string[] = [];
|
|
36
|
+
private localSkills: string[] = [];
|
|
31
37
|
private mcpTools: string[] = [];
|
|
38
|
+
private readonly userSkillsDirectory: string;
|
|
32
39
|
|
|
33
|
-
constructor(private client?: AgentClient) {
|
|
40
|
+
constructor(private client?: AgentClient, userSkillsDirectory = getUserSkillsDirectory()) {
|
|
41
|
+
this.userSkillsDirectory = userSkillsDirectory;
|
|
42
|
+
this.refreshLocalSkills();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private refreshLocalSkills(): void {
|
|
46
|
+
this.localSkills = discoverUserSkillNames(this.userSkillsDirectory);
|
|
47
|
+
const baseSkills = this.client ? this.remoteSkills : this.skills;
|
|
48
|
+
this.skills = [...new Set([...baseSkills, ...this.localSkills])]
|
|
49
|
+
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
50
|
+
}
|
|
34
51
|
|
|
35
52
|
public async refreshSuggestions(): Promise<void> {
|
|
36
53
|
if (!this.client || !this.client.connected) return;
|
|
37
54
|
try {
|
|
38
55
|
const skills = await this.client.getSkills();
|
|
39
56
|
if (skills) {
|
|
40
|
-
this.
|
|
57
|
+
this.remoteSkills = skills.map((s) => s.name);
|
|
41
58
|
}
|
|
42
59
|
} catch {
|
|
43
60
|
// Ignored if client is offline
|
|
@@ -60,6 +77,8 @@ export class Autocompleter {
|
|
|
60
77
|
} catch {
|
|
61
78
|
// Ignored
|
|
62
79
|
}
|
|
80
|
+
|
|
81
|
+
this.refreshLocalSkills();
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
public getSkills(): string[] {
|
|
@@ -75,6 +94,9 @@ export class Autocompleter {
|
|
|
75
94
|
* (line: string) => [string[], string]
|
|
76
95
|
*/
|
|
77
96
|
public completer(line: string): [string[], string] {
|
|
97
|
+
// A skill may be created by another HELIX surface while the REPL is open.
|
|
98
|
+
// Keep Tab completion current without exposing the skill file contents.
|
|
99
|
+
this.refreshLocalSkills();
|
|
78
100
|
const trimmed = line.trimStart();
|
|
79
101
|
|
|
80
102
|
// 1. Skill sub-arguments for /skill <name>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats Markdown text for optimal display in terminal emulators,
|
|
5
|
+
* resolving visual noise from unparsed asterisks (*, **) and headers.
|
|
6
|
+
*/
|
|
7
|
+
export function formatTerminalMarkdown(content: string, plain = false): string {
|
|
8
|
+
if (!content) return '';
|
|
9
|
+
|
|
10
|
+
const lines = content.split('\n');
|
|
11
|
+
const formattedLines = lines.map((line) => {
|
|
12
|
+
let formatted = line;
|
|
13
|
+
|
|
14
|
+
// Headers (#, ##, ###)
|
|
15
|
+
const headerMatch = formatted.match(/^(#{1,6})\s+(.*)$/);
|
|
16
|
+
if (headerMatch) {
|
|
17
|
+
const title = headerMatch[2].trim();
|
|
18
|
+
if (plain) {
|
|
19
|
+
return title.toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
return chalk.hex('#FF9F1C').bold(title);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Bullet lists (* item, - item)
|
|
25
|
+
if (/^\s*[*+-]\s+/.test(formatted)) {
|
|
26
|
+
formatted = formatted.replace(/^(\s*)[*+-]\s+/, (_m, indent) => {
|
|
27
|
+
return plain ? `${indent}- ` : `${indent}${chalk.hex('#FF8C00')('•')} `;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Inline bold (**text** or __text__)
|
|
32
|
+
formatted = formatted.replace(/(\*\*|__)(.*?)\1/g, (_m, _delim, text) => {
|
|
33
|
+
return plain ? text : chalk.bold(text);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Inline italics (*text* or _text_)
|
|
37
|
+
formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)/g, (_m, t1, t2) => {
|
|
38
|
+
const text = t1 || t2;
|
|
39
|
+
return plain ? text : chalk.whiteBright(text);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Inline code (`code`)
|
|
43
|
+
formatted = formatted.replace(/`([^`]+)`/g, (_m, code) => {
|
|
44
|
+
return plain ? code : chalk.cyan(code);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return formatted;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return formattedLines.join('\n');
|
|
51
|
+
}
|
package/src/ui/repl.ts
CHANGED
|
@@ -25,10 +25,13 @@ import { handleCreateMcp, handleListMcp } from '../commands/mcp';
|
|
|
25
25
|
import { handleMicrophoneCommand } from '../commands/mic';
|
|
26
26
|
import { handleBrowserCommand } from '../commands/browser';
|
|
27
27
|
import type { ConfirmationRequest } from '../client/types';
|
|
28
|
+
import { formatTerminalMarkdown } from './markdown';
|
|
29
|
+
import { handleCapabilities, handleProviders, handleSettings } from '../commands/diagnostics';
|
|
28
30
|
|
|
29
31
|
export interface ReplOptions {
|
|
30
32
|
wsUrl: string;
|
|
31
33
|
wsToken?: string;
|
|
34
|
+
plain?: boolean;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export class HelixRepl {
|
|
@@ -43,9 +46,11 @@ export class HelixRepl {
|
|
|
43
46
|
private activeTaskId: string | null = null;
|
|
44
47
|
private activeTasksCount: number = 0;
|
|
45
48
|
private isProcessingPrompt: boolean = false;
|
|
49
|
+
private plain: boolean = false;
|
|
46
50
|
|
|
47
51
|
constructor(private options: ReplOptions) {
|
|
48
52
|
this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
53
|
+
this.plain = Boolean(options.plain);
|
|
49
54
|
this.client = new AgentClient({
|
|
50
55
|
wsUrl: options.wsUrl,
|
|
51
56
|
wsToken: options.wsToken,
|
|
@@ -182,8 +187,9 @@ export class HelixRepl {
|
|
|
182
187
|
console.log('');
|
|
183
188
|
console.log(` ${chalk.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
|
|
184
189
|
|
|
185
|
-
// Content with subtle left border
|
|
186
|
-
const
|
|
190
|
+
// Content with subtle left border and clean Markdown formatting
|
|
191
|
+
const formatted = formatTerminalMarkdown(content, this.plain);
|
|
192
|
+
const contentLines = formatted.trim().split('\n');
|
|
187
193
|
for (const line of contentLines) {
|
|
188
194
|
console.log(` ${chalk.gray('│')} ${line}`);
|
|
189
195
|
}
|
|
@@ -392,6 +398,18 @@ export class HelixRepl {
|
|
|
392
398
|
await this._handleStatus();
|
|
393
399
|
break;
|
|
394
400
|
|
|
401
|
+
case '/providers':
|
|
402
|
+
await handleProviders(this.client);
|
|
403
|
+
break;
|
|
404
|
+
|
|
405
|
+
case '/capabilities':
|
|
406
|
+
await handleCapabilities(this.client);
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case '/settings':
|
|
410
|
+
await handleSettings(this.client);
|
|
411
|
+
break;
|
|
412
|
+
|
|
395
413
|
case '/stop':
|
|
396
414
|
console.log(RED_BOLD('\n[PARADA DE EMERGENCIA]: Cancelando todas las tareas activas...'));
|
|
397
415
|
this.client.emergencyStop();
|
package/tests/cli.test.ts
CHANGED
|
@@ -3,6 +3,10 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { Autocompleter, SLASH_COMMANDS } from '../src/ui/autocomplete';
|
|
4
4
|
import { formatSkill, formatMcp, highlightMentions, ORANGE } from '../src/ui/colors';
|
|
5
5
|
import { getAsciiLogo, getStatusBar, getVoiceIndicator } from '../src/ui/banner';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { discoverUserSkillNames } from '../src/skills/user-skill-discovery';
|
|
6
10
|
|
|
7
11
|
test('Autocompleter suggests slash commands', () => {
|
|
8
12
|
const completer = new Autocompleter();
|
|
@@ -68,6 +72,42 @@ test('Autocompleter handles @skills and #mcps autocompletion', () => {
|
|
|
68
72
|
assert.deepEqual(skillSub, ['/skill web-search']);
|
|
69
73
|
});
|
|
70
74
|
|
|
75
|
+
test('User skill discovery is shallow, dynamic, and metadata-only', () => {
|
|
76
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'helix-cli-skills-'));
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(path.join(root, 'meeting-notes'));
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(root, 'meeting-notes', 'SKILL.md'),
|
|
81
|
+
'---\nname: meeting-notes\n---\nSECRET_INSTRUCTIONS_SHOULD_NOT_BE_READ',
|
|
82
|
+
'utf8',
|
|
83
|
+
);
|
|
84
|
+
fs.mkdirSync(path.join(root, 'not-a-skill.txt'));
|
|
85
|
+
fs.mkdirSync(path.join(root, 'nested', 'hidden-skill'), { recursive: true });
|
|
86
|
+
fs.writeFileSync(path.join(root, 'nested', 'hidden-skill', 'SKILL.md'), 'secret', 'utf8');
|
|
87
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'not a skill', 'utf8');
|
|
88
|
+
|
|
89
|
+
assert.deepEqual(discoverUserSkillNames(root), ['meeting-notes']);
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('Autocomplete merges default/remote skills with newly-created user skills', () => {
|
|
96
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'helix-cli-refresh-'));
|
|
97
|
+
try {
|
|
98
|
+
const completer = new Autocompleter(undefined, root);
|
|
99
|
+
(completer as any).remoteSkills = ['coding'];
|
|
100
|
+
(completer as any).skills = ['coding'];
|
|
101
|
+
fs.mkdirSync(path.join(root, 'custom-review'));
|
|
102
|
+
fs.writeFileSync(path.join(root, 'custom-review', 'SKILL.md'), '# user skill', 'utf8');
|
|
103
|
+
|
|
104
|
+
const [matches] = completer.completer('@custom');
|
|
105
|
+
assert.deepEqual(matches, ['@custom-review ']);
|
|
106
|
+
} finally {
|
|
107
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
71
111
|
test('Colors: formatSkill applies strong orange formatting', () => {
|
|
72
112
|
const formatted = formatSkill('web-researcher');
|
|
73
113
|
assert.ok(formatted.includes('@web-researcher'));
|
|
@@ -185,3 +225,23 @@ test('CLI: run with piped UTF-8 Spanish text with BOM is decoded cleanly', async
|
|
|
185
225
|
});
|
|
186
226
|
});
|
|
187
227
|
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
test('Markdown: formatTerminalMarkdown parses bold, headers and bullets cleanly', async () => {
|
|
231
|
+
const { formatTerminalMarkdown } = await import('../src/ui/markdown');
|
|
232
|
+
const raw = '# Resumen\n\nAquí hay **texto importante** y un *detalle*:\n* Primer punto\n* Segundo punto\n`código`';
|
|
233
|
+
|
|
234
|
+
// Rich format
|
|
235
|
+
const formatted = formatTerminalMarkdown(raw);
|
|
236
|
+
assert.ok(!formatted.includes('**texto importante**'));
|
|
237
|
+
assert.ok(formatted.includes('texto importante'));
|
|
238
|
+
assert.ok(formatted.includes('• Primer punto'));
|
|
239
|
+
|
|
240
|
+
// Plain format
|
|
241
|
+
const plain = formatTerminalMarkdown(raw, true);
|
|
242
|
+
assert.ok(!plain.includes('**'));
|
|
243
|
+
assert.ok(!plain.includes('* Primer punto'));
|
|
244
|
+
assert.ok(plain.includes('- Primer punto'));
|
|
245
|
+
assert.ok(plain.includes('texto importante'));
|
|
246
|
+
assert.ok(plain.includes('RESUMEN'));
|
|
247
|
+
});
|