@johpaz/hive-cli 1.0.6 → 1.0.7
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/package.json +4 -3
- package/src/commands/gateway.ts +39 -15
- package/src/commands/onboard.ts +562 -11
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Hive CLI — Command line interface for the Hive AI Gateway",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hive": "src/index.ts"
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@clack/prompts": "^1.0.1",
|
|
18
|
-
"@johpaz/hive-core": "^1.0.
|
|
18
|
+
"@johpaz/hive-core": "^1.0.7",
|
|
19
|
+
"@johpaz/hive-orchestrator": "workspace:*",
|
|
19
20
|
"js-yaml": "latest"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"typescript": "latest",
|
|
23
24
|
"@types/bun": "latest"
|
|
24
25
|
}
|
|
25
|
-
}
|
|
26
|
+
}
|
package/src/commands/gateway.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { loadConfig, startGateway, logger } from "@johpaz/hive-core";
|
|
1
|
+
import { loadConfig, startGateway, logger, expandConfigPath } from "@johpaz/hive-core";
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { spawn, spawnSync, ChildProcess } from "child_process";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_PID_FILE = path.join(process.env.HOME || "", ".hive", "gateway.pid");
|
|
8
8
|
const LOG_FILE = path.join(process.env.HOME || "", ".hive", "logs", "gateway.log");
|
|
9
9
|
|
|
10
|
+
function getPidFile(): string {
|
|
11
|
+
try {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
return expandConfigPath(config.gateway?.pidFile) ?? DEFAULT_PID_FILE;
|
|
14
|
+
} catch {
|
|
15
|
+
return DEFAULT_PID_FILE;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
function ensureLogDir(): void {
|
|
11
20
|
const logDir = path.dirname(LOG_FILE);
|
|
12
21
|
if (!fs.existsSync(logDir)) {
|
|
@@ -15,22 +24,26 @@ function ensureLogDir(): void {
|
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
function isRunning(): boolean {
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
const pidFile = getPidFile();
|
|
28
|
+
if (!fs.existsSync(pidFile)) return false;
|
|
29
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
20
30
|
if (isNaN(pid)) return false;
|
|
21
31
|
try {
|
|
22
32
|
process.kill(pid, 0);
|
|
23
33
|
return true;
|
|
24
34
|
} catch {
|
|
25
|
-
|
|
35
|
+
try {
|
|
36
|
+
fs.unlinkSync(pidFile);
|
|
37
|
+
} catch { }
|
|
26
38
|
return false;
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
export async function start(flags: string[]): Promise<void> {
|
|
31
43
|
const daemon = flags.includes("--daemon");
|
|
44
|
+
const skipCheck = flags.includes("--skip-check");
|
|
32
45
|
|
|
33
|
-
if (isRunning()) {
|
|
46
|
+
if (!skipCheck && isRunning()) {
|
|
34
47
|
console.log("⚠️ Hive Gateway ya está corriendo");
|
|
35
48
|
return;
|
|
36
49
|
}
|
|
@@ -51,23 +64,30 @@ export async function start(flags: string[]): Promise<void> {
|
|
|
51
64
|
║ ██║ ██║██║ ╚████╔╝ ███████╗ ║
|
|
52
65
|
║ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚══════╝ ║
|
|
53
66
|
║ ║
|
|
54
|
-
║ Personal AI Gateway — v1.0.
|
|
67
|
+
║ Personal AI Gateway — v1.0.7 ║
|
|
55
68
|
╚════════════════════════════════════════════╝
|
|
56
69
|
`);
|
|
57
70
|
|
|
58
71
|
if (daemon) {
|
|
59
72
|
ensureLogDir();
|
|
60
|
-
const child = spawn(process.execPath, [process.argv[1] || "", "start"], {
|
|
73
|
+
const child = spawn(process.execPath, [process.argv[1] || "", "start", "--skip-check"], {
|
|
61
74
|
detached: true,
|
|
62
75
|
stdio: ["ignore", fs.openSync(LOG_FILE, "a"), fs.openSync(LOG_FILE, "a")],
|
|
63
76
|
});
|
|
64
77
|
child.unref();
|
|
65
|
-
fs.writeFileSync(
|
|
78
|
+
fs.writeFileSync(getPidFile(), child.pid?.toString() || "");
|
|
66
79
|
console.log(`✅ Hive Gateway iniciado en modo daemon (PID: ${child.pid})`);
|
|
67
80
|
console.log(` Logs: ${LOG_FILE}`);
|
|
68
81
|
return;
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
try {
|
|
85
|
+
// Start Orchestrator sidecar in the same process
|
|
86
|
+
await import("@johpaz/hive-orchestrator");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn(`⚠️ No se pudo iniciar el Orchestrator de Dashboards: ${(error as Error).message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
await startGateway(config);
|
|
72
92
|
}
|
|
73
93
|
|
|
@@ -77,10 +97,11 @@ export async function stop(): Promise<void> {
|
|
|
77
97
|
return;
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
const
|
|
100
|
+
const pidFile = getPidFile();
|
|
101
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
81
102
|
try {
|
|
82
103
|
process.kill(pid, "SIGTERM");
|
|
83
|
-
fs.unlinkSync(
|
|
104
|
+
fs.unlinkSync(pidFile);
|
|
84
105
|
console.log("✅ Hive Gateway detenido");
|
|
85
106
|
} catch (e) {
|
|
86
107
|
console.error("❌ Error deteniendo el Gateway:", e);
|
|
@@ -99,20 +120,23 @@ export async function status(flags: string[]): Promise<void> {
|
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
const config = loadConfig();
|
|
123
|
+
const pidFile = getPidFile();
|
|
102
124
|
|
|
103
125
|
console.log(`Estado: ${running ? "✅ Corriendo" : "⏹️ Detenido"}`);
|
|
104
126
|
if (running) {
|
|
105
|
-
const pid = fs.readFileSync(
|
|
127
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
106
128
|
console.log(`PID: ${pid}`);
|
|
107
129
|
}
|
|
108
130
|
console.log(`Puerto: ${config.gateway?.port || 18790}`);
|
|
109
131
|
console.log(`Host: ${config.gateway?.host || "127.0.0.1"}`);
|
|
110
|
-
|
|
132
|
+
const provider = config.models?.defaultProvider || "no configurado";
|
|
133
|
+
const model = config.models?.defaults?.[provider] || "no configurado";
|
|
134
|
+
console.log(`Modelo: ${provider} / ${model}`);
|
|
111
135
|
console.log(`Config: ${configPath}`);
|
|
112
136
|
console.log(`Logs: ${LOG_FILE}`);
|
|
113
137
|
|
|
114
138
|
if (flags.includes("--json")) {
|
|
115
|
-
console.log("\n" + JSON.stringify({ running, pid: running ? fs.readFileSync(
|
|
139
|
+
console.log("\n" + JSON.stringify({ running, pid: running ? fs.readFileSync(pidFile, "utf-8").trim() : null, config }, null, 2));
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
142
|
|
|
@@ -122,7 +146,7 @@ export async function reload(): Promise<void> {
|
|
|
122
146
|
return;
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
const pid = parseInt(fs.readFileSync(
|
|
149
|
+
const pid = parseInt(fs.readFileSync(getPidFile(), "utf-8").trim(), 10);
|
|
126
150
|
try {
|
|
127
151
|
process.kill(pid, "SIGHUP");
|
|
128
152
|
console.log("✅ Configuración recargada");
|
package/src/commands/onboard.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as yaml from "js-yaml";
|
|
5
5
|
|
|
6
|
-
const VERSION = "1.0.
|
|
6
|
+
const VERSION = "1.0.7";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_MODELS: Record<string, string> = {
|
|
9
9
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -87,14 +87,29 @@ const AVAILABLE_MODELS: Record<string, Array<{ value: string; label: string; hin
|
|
|
87
87
|
],
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
const BUNDLED_SKILLS = [
|
|
91
|
+
{ name: "web_search", label: "Web Search", hint: "Buscar en la web", default: true },
|
|
92
|
+
{ name: "shell", label: "Shell", hint: "Ejecutar comandos", default: true },
|
|
93
|
+
{ name: "file_manager", label: "File Manager", hint: "Operaciones de archivos", default: true },
|
|
94
|
+
{ name: "http_client", label: "HTTP Client", hint: "Peticiones HTTP", default: true },
|
|
95
|
+
{ name: "memory", label: "Memory", hint: "Memoria persistente", default: true },
|
|
96
|
+
{ name: "cron_manager", label: "Cron Manager", hint: "Tareas programadas", default: false },
|
|
97
|
+
{ name: "system_notify", label: "System Notify", hint: "Notificaciones desktop", default: false },
|
|
98
|
+
{ name: "browser_automation", label: "Browser Automation", hint: "Automatizar navegador", default: false },
|
|
99
|
+
{ name: "context_compact", label: "Context Compact", hint: "Compactar contexto", default: false },
|
|
100
|
+
];
|
|
101
|
+
|
|
90
102
|
interface OnboardConfig {
|
|
91
103
|
agentName: string;
|
|
104
|
+
userName: string;
|
|
105
|
+
userId: string;
|
|
92
106
|
provider: string;
|
|
93
107
|
model: string;
|
|
94
108
|
apiKey: string;
|
|
95
109
|
channel: string;
|
|
96
110
|
channelToken: string;
|
|
97
111
|
workspace: string;
|
|
112
|
+
skills: string[];
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
function generateToken(): string {
|
|
@@ -106,6 +121,42 @@ function generateToken(): string {
|
|
|
106
121
|
return token;
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
function generateUserId(): string {
|
|
125
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
126
|
+
let token = "user_";
|
|
127
|
+
for (let i = 0; i < 12; i++) {
|
|
128
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
129
|
+
}
|
|
130
|
+
return token;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function verifyTelegramToken(token: string): Promise<{ ok: boolean; username?: string }> {
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(
|
|
136
|
+
`https://api.telegram.org/bot${token}/getMe`,
|
|
137
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
138
|
+
);
|
|
139
|
+
const data = (await res.json()) as {
|
|
140
|
+
ok: boolean;
|
|
141
|
+
result?: { username: string; first_name: string };
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
ok: data.ok,
|
|
145
|
+
username: data.result?.username,
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return { ok: false };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateDiscordToken(token: string): boolean {
|
|
153
|
+
return token.length >= 50;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateSlackToken(token: string): boolean {
|
|
157
|
+
return token.startsWith("xoxb-") || token.startsWith("xoxp-");
|
|
158
|
+
}
|
|
159
|
+
|
|
109
160
|
async function testLLMConnection(provider: string, apiKey: string, model: string): Promise<boolean> {
|
|
110
161
|
if (provider === "ollama") {
|
|
111
162
|
try {
|
|
@@ -254,6 +305,11 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
|
|
|
254
305
|
const configObj: Record<string, unknown> = {
|
|
255
306
|
name: config.agentName,
|
|
256
307
|
version: VERSION,
|
|
308
|
+
user: {
|
|
309
|
+
id: config.userId,
|
|
310
|
+
name: config.userName,
|
|
311
|
+
channels: {},
|
|
312
|
+
},
|
|
257
313
|
gateway: {
|
|
258
314
|
host: "127.0.0.1",
|
|
259
315
|
port: 18790,
|
|
@@ -279,7 +335,9 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
|
|
|
279
335
|
},
|
|
280
336
|
channels: {},
|
|
281
337
|
skills: {
|
|
282
|
-
allowBundled:
|
|
338
|
+
allowBundled: config.skills,
|
|
339
|
+
managedDir: path.join(process.env.HOME || "", ".hive", "skills"),
|
|
340
|
+
hotReload: true,
|
|
283
341
|
},
|
|
284
342
|
sessions: {
|
|
285
343
|
pruneAfterHours: 168,
|
|
@@ -297,6 +355,7 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
|
|
|
297
355
|
accounts: {
|
|
298
356
|
default: {
|
|
299
357
|
botToken: config.channelToken,
|
|
358
|
+
dmPolicy: "open",
|
|
300
359
|
},
|
|
301
360
|
},
|
|
302
361
|
},
|
|
@@ -317,7 +376,7 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
|
|
|
317
376
|
fs.chmodSync(configPath, 0o600);
|
|
318
377
|
}
|
|
319
378
|
|
|
320
|
-
async function generateWorkspace(workspace: string, agentName: string, ethicsChoice: string): Promise<void> {
|
|
379
|
+
async function generateWorkspace(workspace: string, agentName: string, userName: string, userId: string, userLanguage: string, userTimezone: string, ethicsChoice: string): Promise<void> {
|
|
321
380
|
if (!fs.existsSync(workspace)) {
|
|
322
381
|
fs.mkdirSync(workspace, { recursive: true });
|
|
323
382
|
}
|
|
@@ -355,16 +414,39 @@ Help the user with their tasks, answer questions, and provide assistance.
|
|
|
355
414
|
if (!fs.existsSync(userPath)) {
|
|
356
415
|
fs.writeFileSync(
|
|
357
416
|
userPath,
|
|
358
|
-
`#
|
|
417
|
+
`# ${userName} — Perfil de Usuario
|
|
418
|
+
|
|
419
|
+
## Identidad
|
|
420
|
+
|
|
421
|
+
- **Nombre:** ${userName}
|
|
422
|
+
- **ID Interno:** \`${userId}\` (este ID es para uso interno del sistema)
|
|
423
|
+
- **Idioma preferido:** ${userLanguage}
|
|
424
|
+
- **Zona horaria:** ${userTimezone}
|
|
425
|
+
|
|
426
|
+
## Canales Vinculados
|
|
427
|
+
|
|
428
|
+
Tu ID único (\`${userId}\`) se usa para unificar conversaciones entre canales.
|
|
359
429
|
|
|
360
|
-
|
|
430
|
+
| Canal | ID de Usuario |
|
|
431
|
+
|-------|---------------|
|
|
432
|
+
| Telegram | - |
|
|
433
|
+
| WhatsApp | - |
|
|
434
|
+
| Discord | - |
|
|
435
|
+
| Slack | - |
|
|
361
436
|
|
|
362
|
-
|
|
363
|
-
|
|
437
|
+
Para vincular un canal, usa:
|
|
438
|
+
\`\`\`bash
|
|
439
|
+
hive config set user.channels.telegram "tu_telegram_id"
|
|
440
|
+
\`\`\`
|
|
364
441
|
|
|
365
|
-
##
|
|
442
|
+
## Preferencias
|
|
366
443
|
|
|
367
|
-
|
|
444
|
+
- Idioma: ${userLanguage}
|
|
445
|
+
- Zona horaria: ${userTimezone}
|
|
446
|
+
|
|
447
|
+
## Notas
|
|
448
|
+
|
|
449
|
+
Notas personales sobre ti.
|
|
368
450
|
`,
|
|
369
451
|
"utf-8"
|
|
370
452
|
);
|
|
@@ -435,7 +517,345 @@ WantedBy=default.target
|
|
|
435
517
|
spawnSync("systemctl", ["--user", "enable", "hive"], { stdio: "inherit" });
|
|
436
518
|
}
|
|
437
519
|
|
|
438
|
-
|
|
520
|
+
async function isGatewayRunning(): Promise<boolean> {
|
|
521
|
+
const pidFile = path.join(process.env.HOME || "", ".hive", "gateway.pid");
|
|
522
|
+
if (!fs.existsSync(pidFile)) return false;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
526
|
+
process.kill(pid, 0);
|
|
527
|
+
return true;
|
|
528
|
+
} catch {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function reloadGateway(): Promise<void> {
|
|
534
|
+
const pidFile = path.join(process.env.HOME || "", ".hive", "gateway.pid");
|
|
535
|
+
if (!fs.existsSync(pidFile)) return;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
539
|
+
process.kill(pid, "SIGHUP");
|
|
540
|
+
} catch {
|
|
541
|
+
// Gateway not running
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
interface ExistingConfig {
|
|
546
|
+
agentName: string;
|
|
547
|
+
provider: string;
|
|
548
|
+
model: string;
|
|
549
|
+
channels: string[];
|
|
550
|
+
apiKey: string;
|
|
551
|
+
workspace: string;
|
|
552
|
+
skills: string[];
|
|
553
|
+
raw: Record<string, unknown>;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function parseExistingConfig(raw: Record<string, unknown>): ExistingConfig {
|
|
557
|
+
const agents = (raw.agents as Record<string, unknown>)?.list as Array<Record<string, unknown>> | undefined;
|
|
558
|
+
const models = raw.models as Record<string, unknown> | undefined;
|
|
559
|
+
const providers = models?.providers as Record<string, Record<string, unknown>> | undefined;
|
|
560
|
+
const defaultProvider = models?.defaultProvider as string | undefined;
|
|
561
|
+
const providerConfig = providers?.[defaultProvider || ""] as Record<string, unknown> | undefined;
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
agentName: (agents?.[0]?.name as string) ?? "Hive",
|
|
565
|
+
provider: defaultProvider ?? "gemini",
|
|
566
|
+
model: (models?.defaults as Record<string, string>)?.[defaultProvider || ""] ?? "",
|
|
567
|
+
channels: Object.keys((raw.channels as Record<string, unknown>) ?? {}),
|
|
568
|
+
apiKey: (providerConfig?.apiKey as string) ?? "",
|
|
569
|
+
workspace: (agents?.[0]?.workspace as string) ?? path.join(process.env.HOME || "", ".hive", "workspace"),
|
|
570
|
+
skills: ((raw.skills as Record<string, unknown>)?.allowBundled as string[]) ?? [],
|
|
571
|
+
raw,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function runUpdateWizard(configPath: string, existing: ExistingConfig): Promise<void> {
|
|
576
|
+
p.note(
|
|
577
|
+
"Presiona Enter para mantener el valor actual de cada campo.",
|
|
578
|
+
"✏️ Modo actualización"
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// Nombre del agente
|
|
582
|
+
const agentName = await p.text({
|
|
583
|
+
message: "Nombre del agente:",
|
|
584
|
+
placeholder: existing.agentName,
|
|
585
|
+
defaultValue: existing.agentName,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (p.isCancel(agentName)) {
|
|
589
|
+
p.cancel("Actualización cancelada.");
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Proveedor LLM
|
|
594
|
+
const changeProvider = await p.confirm({
|
|
595
|
+
message: `Proveedor actual: ${existing.provider} (${existing.model || "no configurado"}). ¿Cambiar?`,
|
|
596
|
+
initialValue: false,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (p.isCancel(changeProvider)) {
|
|
600
|
+
p.cancel("Actualización cancelada.");
|
|
601
|
+
process.exit(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let provider = existing.provider;
|
|
605
|
+
let model = existing.model;
|
|
606
|
+
let apiKey = existing.apiKey;
|
|
607
|
+
|
|
608
|
+
if (changeProvider) {
|
|
609
|
+
provider = await p.select({
|
|
610
|
+
message: "Nuevo proveedor LLM:",
|
|
611
|
+
options: [
|
|
612
|
+
{ value: "anthropic", label: "Anthropic (Claude)", hint: "Recomendado — Claude 4.6" },
|
|
613
|
+
{ value: "openai", label: "OpenAI (GPT-5)", hint: "GPT-5.2" },
|
|
614
|
+
{ value: "gemini", label: "Google Gemini", hint: "Gemini 3 Flash" },
|
|
615
|
+
{ value: "deepseek", label: "DeepSeek", hint: "Muy económico" },
|
|
616
|
+
{ value: "kimi", label: "Kimi (Moonshot AI)", hint: "Contexto largo" },
|
|
617
|
+
{ value: "openrouter", label: "OpenRouter", hint: "Multi-modelo" },
|
|
618
|
+
{ value: "ollama", label: "Ollama (local)", hint: "Sin costo" },
|
|
619
|
+
],
|
|
620
|
+
}) as string;
|
|
621
|
+
|
|
622
|
+
if (p.isCancel(provider)) {
|
|
623
|
+
p.cancel("Actualización cancelada.");
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const models = AVAILABLE_MODELS[provider] || [{ value: DEFAULT_MODELS[provider], label: DEFAULT_MODELS[provider] }];
|
|
628
|
+
|
|
629
|
+
if (models.length > 1) {
|
|
630
|
+
model = await p.select({
|
|
631
|
+
message: `Modelo de ${provider}:`,
|
|
632
|
+
options: models,
|
|
633
|
+
}) as string;
|
|
634
|
+
|
|
635
|
+
if (p.isCancel(model)) {
|
|
636
|
+
p.cancel("Actualización cancelada.");
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
model = models[0].value;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (provider !== "ollama") {
|
|
644
|
+
const link = API_KEY_LINKS[provider];
|
|
645
|
+
if (link) {
|
|
646
|
+
p.note(`Obtén tu API key en:\n${link}`, `API key de ${provider}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const keyResult = await p.text({
|
|
650
|
+
message: `API key de ${provider}:`,
|
|
651
|
+
placeholder: API_KEY_PLACEHOLDERS[provider] || "sk-...",
|
|
652
|
+
validate: (v: string) => (!v || v.length < 10 ? "La key parece muy corta" : undefined),
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
if (p.isCancel(keyResult)) {
|
|
656
|
+
p.cancel("Actualización cancelada.");
|
|
657
|
+
process.exit(0);
|
|
658
|
+
}
|
|
659
|
+
apiKey = keyResult;
|
|
660
|
+
|
|
661
|
+
const spinner = p.spinner();
|
|
662
|
+
spinner.start(`Verificando conexión con ${provider}...`);
|
|
663
|
+
|
|
664
|
+
const connected = await testLLMConnection(provider, apiKey, model as string);
|
|
665
|
+
|
|
666
|
+
if (!connected) {
|
|
667
|
+
spinner.stop(`❌ Error conectando con ${provider}`);
|
|
668
|
+
p.outro("API key inválida. Ejecuta 'hive onboard' de nuevo con la key correcta.");
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
spinner.stop(`✅ Conexión con ${provider} verificada`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Canales
|
|
677
|
+
const currentChannels = existing.channels.length > 0 ? existing.channels.join(", ") : "ninguno";
|
|
678
|
+
const changeChannel = await p.confirm({
|
|
679
|
+
message: `Canales configurados: ${currentChannels}. ¿Añadir o cambiar?`,
|
|
680
|
+
initialValue: false,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (p.isCancel(changeChannel)) {
|
|
684
|
+
p.cancel("Actualización cancelada.");
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
let channels = existing.raw.channels as Record<string, unknown> | undefined;
|
|
689
|
+
|
|
690
|
+
if (changeChannel) {
|
|
691
|
+
const channel = await p.select({
|
|
692
|
+
message: "Canal a configurar:",
|
|
693
|
+
options: [
|
|
694
|
+
{ value: "telegram", label: "Telegram", hint: "Recomendado" },
|
|
695
|
+
{ value: "discord", label: "Discord" },
|
|
696
|
+
{ value: "webchat", label: "WebChat (local)" },
|
|
697
|
+
{ value: "none", label: "Ninguno" },
|
|
698
|
+
],
|
|
699
|
+
}) as string;
|
|
700
|
+
|
|
701
|
+
if (p.isCancel(channel)) {
|
|
702
|
+
p.cancel("Actualización cancelada.");
|
|
703
|
+
process.exit(0);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (channel === "telegram") {
|
|
707
|
+
p.note(
|
|
708
|
+
"1. Abre Telegram y busca @BotFather\n" +
|
|
709
|
+
"2. Escribe /newbot y sigue las instrucciones\n" +
|
|
710
|
+
"3. Copia el token que te da BotFather",
|
|
711
|
+
"Cómo obtener el token de Telegram"
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
const tokenResult = await p.text({
|
|
715
|
+
message: "Token de Telegram BotFather:",
|
|
716
|
+
placeholder: "123456789:ABCdefGHI...",
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
if (p.isCancel(tokenResult)) {
|
|
720
|
+
p.cancel("Actualización cancelada.");
|
|
721
|
+
process.exit(0);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const spinner = p.spinner();
|
|
725
|
+
spinner.start("Verificando token de Telegram...");
|
|
726
|
+
const tg = await verifyTelegramToken(tokenResult as string);
|
|
727
|
+
if (tg.ok && tg.username) {
|
|
728
|
+
spinner.stop(`✅ Bot verificado: @${tg.username}`);
|
|
729
|
+
} else {
|
|
730
|
+
spinner.stop("⚠️ Token no verificado — se guardará de todas formas");
|
|
731
|
+
p.note(
|
|
732
|
+
"El token se guardó pero no pudo verificarse.\n" +
|
|
733
|
+
"Si es incorrecto, ejecuta: hive onboard (opción actualizar)",
|
|
734
|
+
"Aviso"
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const telegramAccount: Record<string, unknown> = {
|
|
739
|
+
botToken: tokenResult,
|
|
740
|
+
dmPolicy: "open",
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
channels = {
|
|
744
|
+
telegram: {
|
|
745
|
+
accounts: {
|
|
746
|
+
default: telegramAccount,
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
} else if (channel === "discord") {
|
|
751
|
+
p.note(
|
|
752
|
+
"1. Ve a https://discord.com/developers/applications\n" +
|
|
753
|
+
"2. Crea una nueva aplicación\n" +
|
|
754
|
+
"3. Ve a Bot → Reset Token\n" +
|
|
755
|
+
"4. Habilita 'Message Content Intent'",
|
|
756
|
+
"Cómo obtener el token de Discord"
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const tokenResult = await p.text({
|
|
760
|
+
message: "Token del bot de Discord:",
|
|
761
|
+
placeholder: "MTk4NjIyNDgzNDcxO...",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (p.isCancel(tokenResult)) {
|
|
765
|
+
p.cancel("Actualización cancelada.");
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
channels = {
|
|
770
|
+
discord: {
|
|
771
|
+
accounts: {
|
|
772
|
+
default: {
|
|
773
|
+
token: tokenResult,
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Guardar cambios
|
|
782
|
+
const spinner = p.spinner();
|
|
783
|
+
spinner.start("Guardando cambios...");
|
|
784
|
+
|
|
785
|
+
const baseUrlMap: Record<string, string> = {
|
|
786
|
+
gemini: "https://generativelanguage.googleapis.com/v1beta",
|
|
787
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
788
|
+
kimi: "https://api.moonshot.cn/v1",
|
|
789
|
+
ollama: "http://localhost:11434/api",
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const providersConfig: Record<string, Record<string, unknown>> = {};
|
|
793
|
+
if (provider !== "ollama" && apiKey) {
|
|
794
|
+
providersConfig[provider] = { apiKey };
|
|
795
|
+
if (baseUrlMap[provider]) {
|
|
796
|
+
providersConfig[provider].baseUrl = baseUrlMap[provider];
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const updatedConfig: Record<string, unknown> = {
|
|
801
|
+
...existing.raw,
|
|
802
|
+
name: agentName,
|
|
803
|
+
agents: {
|
|
804
|
+
list: [
|
|
805
|
+
{
|
|
806
|
+
id: "main",
|
|
807
|
+
default: true,
|
|
808
|
+
name: agentName,
|
|
809
|
+
workspace: existing.workspace,
|
|
810
|
+
agentDir: path.join(process.env.HOME || "", ".hive", "agents", "main", "agent"),
|
|
811
|
+
},
|
|
812
|
+
],
|
|
813
|
+
},
|
|
814
|
+
models: {
|
|
815
|
+
defaultProvider: provider,
|
|
816
|
+
defaults: {
|
|
817
|
+
[provider]: model,
|
|
818
|
+
},
|
|
819
|
+
providers: providersConfig,
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (channels) {
|
|
824
|
+
updatedConfig.channels = channels;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
fs.writeFileSync(configPath, yaml.dump(updatedConfig, { lineWidth: -1 }), "utf-8");
|
|
828
|
+
fs.chmodSync(configPath, 0o600);
|
|
829
|
+
|
|
830
|
+
spinner.stop("Cambios guardados ✅");
|
|
831
|
+
|
|
832
|
+
// Recargar Gateway si está corriendo
|
|
833
|
+
const running = await isGatewayRunning();
|
|
834
|
+
if (running) {
|
|
835
|
+
const reload = await p.confirm({
|
|
836
|
+
message: "El Gateway está corriendo. ¿Recargar configuración ahora?",
|
|
837
|
+
initialValue: true,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (reload) {
|
|
841
|
+
await reloadGateway();
|
|
842
|
+
p.log.success("Configuración recargada ✅");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const channelDisplay = channels ? Object.keys(channels).join(", ") || "ninguno" : existing.channels.join(", ") || "ninguno";
|
|
847
|
+
|
|
848
|
+
p.outro(
|
|
849
|
+
`✅ Configuración actualizada.\n\n` +
|
|
850
|
+
` Agente: ${agentName}\n` +
|
|
851
|
+
` Proveedor: ${provider} (${model})\n` +
|
|
852
|
+
` Canales: ${channelDisplay}\n\n` +
|
|
853
|
+
` hive status → ver estado actual\n` +
|
|
854
|
+
` hive reload → recargar manualmente`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function runFullWizard(configPath: string): Promise<void> {
|
|
439
859
|
p.intro("🐝 Bienvenido a Hive — Personal AI Gateway");
|
|
440
860
|
|
|
441
861
|
const agentName = await p.text({
|
|
@@ -449,6 +869,44 @@ export async function onboard(): Promise<void> {
|
|
|
449
869
|
process.exit(0);
|
|
450
870
|
}
|
|
451
871
|
|
|
872
|
+
const userName = await p.text({
|
|
873
|
+
message: "¿Cómo te llamas? (nombre para identificarte)",
|
|
874
|
+
placeholder: "Usuario",
|
|
875
|
+
defaultValue: "Usuario",
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
if (p.isCancel(userName)) {
|
|
879
|
+
p.cancel("Onboarding cancelado.");
|
|
880
|
+
process.exit(0);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const userLanguage = await p.select({
|
|
884
|
+
message: "¿En qué idioma prefieres que te responda?",
|
|
885
|
+
options: [
|
|
886
|
+
{ value: "Spanish", label: "Español" },
|
|
887
|
+
{ value: "English", label: "English" },
|
|
888
|
+
{ value: "Spanish, English", label: "Ambos / Both" },
|
|
889
|
+
],
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
if (p.isCancel(userLanguage)) {
|
|
893
|
+
p.cancel("Onboarding cancelado.");
|
|
894
|
+
process.exit(0);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const userTimezone = await p.text({
|
|
898
|
+
message: "¿Cuál es tu zona horaria? (ej: America/Bogota, Europe/Madrid)",
|
|
899
|
+
placeholder: "America/Bogota",
|
|
900
|
+
defaultValue: "America/Bogota",
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
if (p.isCancel(userTimezone)) {
|
|
904
|
+
p.cancel("Onboarding cancelado.");
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const userId = generateUserId();
|
|
909
|
+
|
|
452
910
|
const provider = await p.select({
|
|
453
911
|
message: "¿Qué proveedor LLM quieres usar?",
|
|
454
912
|
options: [
|
|
@@ -619,6 +1077,23 @@ export async function onboard(): Promise<void> {
|
|
|
619
1077
|
process.exit(0);
|
|
620
1078
|
}
|
|
621
1079
|
channelToken = tokenResult;
|
|
1080
|
+
|
|
1081
|
+
const verifySpinner = p.spinner();
|
|
1082
|
+
verifySpinner.start("Verificando token de Telegram...");
|
|
1083
|
+
const tg = await verifyTelegramToken(channelToken);
|
|
1084
|
+
if (tg.ok && tg.username) {
|
|
1085
|
+
verifySpinner.stop(`✅ Bot verificado: @${tg.username}`);
|
|
1086
|
+
} else {
|
|
1087
|
+
verifySpinner.stop("⚠️ Token no verificado — se guardará de todas formas");
|
|
1088
|
+
p.note(
|
|
1089
|
+
"El token no pudo verificarse. Si es incorrecto, ejecuta 'hive onboard' después.\n\n" +
|
|
1090
|
+
"Para obtener tu Telegram ID y autorizarte:\n" +
|
|
1091
|
+
"1. Inicia el bot con 'hive start'\n" +
|
|
1092
|
+
"2. Escribe /myid al bot\n" +
|
|
1093
|
+
"3. Añade tu ID a ~/.hive/hive.yaml",
|
|
1094
|
+
"Próximos pasos"
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
622
1097
|
} else if (channel === "discord") {
|
|
623
1098
|
p.note(
|
|
624
1099
|
"1. Ve a https://discord.com/developers/applications\n" +
|
|
@@ -640,6 +1115,23 @@ export async function onboard(): Promise<void> {
|
|
|
640
1115
|
}
|
|
641
1116
|
}
|
|
642
1117
|
|
|
1118
|
+
// Skills selection
|
|
1119
|
+
p.note("Skills son capacidades predefinidas que el agente puede usar.\nActiva las que necesites.", "Skills disponibles");
|
|
1120
|
+
const selectedSkills = await p.multiselect({
|
|
1121
|
+
message: "¿Qué skills deseas activar?",
|
|
1122
|
+
options: BUNDLED_SKILLS.map(s => ({
|
|
1123
|
+
value: s.name,
|
|
1124
|
+
label: s.label,
|
|
1125
|
+
hint: s.hint,
|
|
1126
|
+
})),
|
|
1127
|
+
initialValues: BUNDLED_SKILLS.filter(s => s.default).map(s => s.name),
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
if (p.isCancel(selectedSkills)) {
|
|
1131
|
+
p.cancel("Onboarding cancelado.");
|
|
1132
|
+
process.exit(0);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
643
1135
|
const installService = await p.confirm({
|
|
644
1136
|
message: "¿Instalar Hive como servicio del sistema? (arranca automáticamente)",
|
|
645
1137
|
initialValue: false,
|
|
@@ -655,15 +1147,18 @@ export async function onboard(): Promise<void> {
|
|
|
655
1147
|
|
|
656
1148
|
await generateConfig({
|
|
657
1149
|
agentName: agentName as string,
|
|
1150
|
+
userName: userName as string,
|
|
1151
|
+
userId: userId,
|
|
658
1152
|
provider: providerKey,
|
|
659
1153
|
model: model as string,
|
|
660
1154
|
apiKey,
|
|
661
1155
|
channel: channel as string,
|
|
662
1156
|
channelToken,
|
|
663
1157
|
workspace: workspace as string,
|
|
1158
|
+
skills: selectedSkills as string[],
|
|
664
1159
|
});
|
|
665
1160
|
|
|
666
|
-
await generateWorkspace(workspace as string, agentName as string, ethicsChoice as string);
|
|
1161
|
+
await generateWorkspace(workspace as string, agentName as string, userName as string, userId, userLanguage as string, userTimezone as string, ethicsChoice as string);
|
|
667
1162
|
|
|
668
1163
|
spinner.stop("Configuración creada ✅");
|
|
669
1164
|
|
|
@@ -699,3 +1194,59 @@ export async function onboard(): Promise<void> {
|
|
|
699
1194
|
` hive agents add <nombre>`
|
|
700
1195
|
);
|
|
701
1196
|
}
|
|
1197
|
+
|
|
1198
|
+
export async function onboard(): Promise<void> {
|
|
1199
|
+
const hiveDir = path.join(process.env.HOME || "", ".hive");
|
|
1200
|
+
const configPath = path.join(hiveDir, "hive.yaml");
|
|
1201
|
+
|
|
1202
|
+
if (fs.existsSync(configPath)) {
|
|
1203
|
+
p.intro("🔍 Configuración existente detectada");
|
|
1204
|
+
|
|
1205
|
+
const rawConfig = yaml.load(fs.readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
1206
|
+
const existing = parseExistingConfig(rawConfig);
|
|
1207
|
+
|
|
1208
|
+
p.note(
|
|
1209
|
+
` Agente: ${existing.agentName}\n` +
|
|
1210
|
+
` Proveedor: ${existing.provider} (${existing.model || "no configurado"})\n` +
|
|
1211
|
+
` Canales: ${existing.channels.length > 0 ? existing.channels.join(", ") : "ninguno"}\n` +
|
|
1212
|
+
` Workspace: ${existing.workspace}`,
|
|
1213
|
+
"Config actual"
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
const action = await p.select({
|
|
1217
|
+
message: "¿Qué quieres hacer?",
|
|
1218
|
+
options: [
|
|
1219
|
+
{ value: "update", label: "Actualizar", hint: "Modificar algunos campos" },
|
|
1220
|
+
{ value: "reset", label: "Reiniciar", hint: "Crear config desde cero" },
|
|
1221
|
+
{ value: "cancel", label: "Cancelar", hint: "Salir sin cambios" },
|
|
1222
|
+
],
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
if (p.isCancel(action) || action === "cancel") {
|
|
1226
|
+
p.cancel("Operación cancelada.");
|
|
1227
|
+
process.exit(0);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (action === "reset") {
|
|
1231
|
+
const confirm = await p.confirm({
|
|
1232
|
+
message: "⚠️ Esto borrará tu config actual. ¿Continuar?",
|
|
1233
|
+
initialValue: false,
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
1237
|
+
p.cancel("Operación cancelada.");
|
|
1238
|
+
process.exit(0);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const backupPath = path.join(hiveDir, `hive.yaml.backup.${Date.now()}`);
|
|
1242
|
+
fs.copyFileSync(configPath, backupPath);
|
|
1243
|
+
p.log.info(`Backup creado: ${backupPath}`);
|
|
1244
|
+
|
|
1245
|
+
await runFullWizard(configPath);
|
|
1246
|
+
} else {
|
|
1247
|
+
await runUpdateWizard(configPath, existing);
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
await runFullWizard(configPath);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { securityAudit } from "./commands/security";
|
|
|
14
14
|
import { installService } from "./commands/service";
|
|
15
15
|
import { update } from "./commands/update";
|
|
16
16
|
|
|
17
|
-
const VERSION = "1.0.
|
|
17
|
+
const VERSION = "1.0.7";
|
|
18
18
|
|
|
19
19
|
const HELP = `
|
|
20
20
|
🐝 Hive — Personal AI Gateway v${VERSION}
|