@johpaz/hive-cli 1.0.6 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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.6",
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
+ }
@@ -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 PID_FILE = path.join(process.env.HOME || "", ".hive", "hive.pid");
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
- if (!fs.existsSync(PID_FILE)) return false;
19
- const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
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
- fs.unlinkSync(PID_FILE);
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.6
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(PID_FILE, child.pid?.toString() || "");
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 pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
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(PID_FILE);
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(PID_FILE, "utf-8").trim();
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
- console.log(`Modelo: ${config.models?.defaultProvider || "no configurado"} / ${(config.models?.defaults as Record<string, string>)?.default || "no configurado"}`);
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(PID_FILE, "utf-8").trim() : null, config }, null, 2));
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(PID_FILE, "utf-8").trim(), 10);
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");
@@ -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";
6
+ const VERSION = "1.0.8";
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
- `# User Profile
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
- ## Preferences
430
+ | Canal | ID de Usuario |
431
+ |-------|---------------|
432
+ | Telegram | - |
433
+ | WhatsApp | - |
434
+ | Discord | - |
435
+ | Slack | - |
361
436
 
362
- - Language: Spanish
363
- - Timezone: Auto-detect
437
+ Para vincular un canal, usa:
438
+ \`\`\`bash
439
+ hive config set user.channels.telegram "tu_telegram_id"
440
+ \`\`\`
364
441
 
365
- ## Notes
442
+ ## Preferencias
366
443
 
367
- Add personal notes about the user here.
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
- export async function onboard(): Promise<void> {
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.6";
17
+ const VERSION = "1.0.7";
18
18
 
19
19
  const HELP = `
20
20
  🐝 Hive — Personal AI Gateway v${VERSION}