@johpaz/hive-cli 1.0.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-cli",
3
- "version": "1.0.5",
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.5",
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.5
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.5";
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 {
@@ -229,18 +280,47 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
229
280
  fs.mkdirSync(hiveDir, { recursive: true });
230
281
  }
231
282
 
283
+ const baseUrlMap: Record<string, string> = {
284
+ gemini: "https://generativelanguage.googleapis.com/v1beta",
285
+ deepseek: "https://api.deepseek.com/v1",
286
+ kimi: "https://api.moonshot.cn/v1",
287
+ ollama: "http://localhost:11434/api",
288
+ };
289
+
290
+ const providersConfig: Record<string, Record<string, unknown>> = {};
291
+
292
+ if (config.provider !== "ollama" && config.apiKey) {
293
+ providersConfig[config.provider] = {
294
+ apiKey: config.apiKey,
295
+ };
296
+ if (baseUrlMap[config.provider]) {
297
+ providersConfig[config.provider].baseUrl = baseUrlMap[config.provider];
298
+ }
299
+ } else if (config.provider === "ollama") {
300
+ providersConfig[config.provider] = {
301
+ baseUrl: baseUrlMap[config.provider],
302
+ };
303
+ }
304
+
232
305
  const configObj: Record<string, unknown> = {
233
306
  name: config.agentName,
234
307
  version: VERSION,
308
+ user: {
309
+ id: config.userId,
310
+ name: config.userName,
311
+ channels: {},
312
+ },
235
313
  gateway: {
236
- port: 18790,
237
314
  host: "127.0.0.1",
238
- token: generateToken(),
315
+ port: 18790,
316
+ authToken: generateToken(),
239
317
  },
240
- model: {
241
- provider: config.provider,
242
- name: config.model,
243
- apiKey: config.provider === "ollama" ? "" : config.apiKey,
318
+ models: {
319
+ defaultProvider: config.provider,
320
+ defaults: {
321
+ [config.provider]: config.model,
322
+ },
323
+ providers: providersConfig,
244
324
  },
245
325
  agents: {
246
326
  list: [
@@ -255,13 +335,12 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
255
335
  },
256
336
  channels: {},
257
337
  skills: {
258
- watch: true,
259
- allowBundled: [],
260
- denyBundled: [],
338
+ allowBundled: config.skills,
339
+ managedDir: path.join(process.env.HOME || "", ".hive", "skills"),
340
+ hotReload: true,
261
341
  },
262
342
  sessions: {
263
343
  pruneAfterHours: 168,
264
- pruneInterval: 24,
265
344
  },
266
345
  logging: {
267
346
  level: "info",
@@ -270,22 +349,13 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
270
349
  },
271
350
  };
272
351
 
273
- if (config.provider === "gemini") {
274
- (configObj.model as Record<string, unknown>).baseUrl = "https://generativelanguage.googleapis.com/v1beta";
275
- } else if (config.provider === "deepseek") {
276
- (configObj.model as Record<string, unknown>).baseUrl = "https://api.deepseek.com/v1";
277
- } else if (config.provider === "kimi") {
278
- (configObj.model as Record<string, unknown>).baseUrl = "https://api.moonshot.cn/v1";
279
- } else if (config.provider === "ollama") {
280
- (configObj.model as Record<string, unknown>).baseUrl = "http://localhost:11434/api";
281
- }
282
-
283
352
  if (config.channel === "telegram" && config.channelToken) {
284
353
  configObj.channels = {
285
354
  telegram: {
286
355
  accounts: {
287
356
  default: {
288
357
  botToken: config.channelToken,
358
+ dmPolicy: "open",
289
359
  },
290
360
  },
291
361
  },
@@ -306,7 +376,7 @@ async function generateConfig(config: OnboardConfig): Promise<void> {
306
376
  fs.chmodSync(configPath, 0o600);
307
377
  }
308
378
 
309
- 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> {
310
380
  if (!fs.existsSync(workspace)) {
311
381
  fs.mkdirSync(workspace, { recursive: true });
312
382
  }
@@ -344,16 +414,39 @@ Help the user with their tasks, answer questions, and provide assistance.
344
414
  if (!fs.existsSync(userPath)) {
345
415
  fs.writeFileSync(
346
416
  userPath,
347
- `# 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.
429
+
430
+ | Canal | ID de Usuario |
431
+ |-------|---------------|
432
+ | Telegram | - |
433
+ | WhatsApp | - |
434
+ | Discord | - |
435
+ | Slack | - |
436
+
437
+ Para vincular un canal, usa:
438
+ \`\`\`bash
439
+ hive config set user.channels.telegram "tu_telegram_id"
440
+ \`\`\`
348
441
 
349
- ## Preferences
442
+ ## Preferencias
350
443
 
351
- - Language: Spanish
352
- - Timezone: Auto-detect
444
+ - Idioma: ${userLanguage}
445
+ - Zona horaria: ${userTimezone}
353
446
 
354
- ## Notes
447
+ ## Notas
355
448
 
356
- Add personal notes about the user here.
449
+ Notas personales sobre ti.
357
450
  `,
358
451
  "utf-8"
359
452
  );
@@ -424,7 +517,345 @@ WantedBy=default.target
424
517
  spawnSync("systemctl", ["--user", "enable", "hive"], { stdio: "inherit" });
425
518
  }
426
519
 
427
- 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> {
428
859
  p.intro("🐝 Bienvenido a Hive — Personal AI Gateway");
429
860
 
430
861
  const agentName = await p.text({
@@ -438,6 +869,44 @@ export async function onboard(): Promise<void> {
438
869
  process.exit(0);
439
870
  }
440
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
+
441
910
  const provider = await p.select({
442
911
  message: "¿Qué proveedor LLM quieres usar?",
443
912
  options: [
@@ -608,6 +1077,23 @@ export async function onboard(): Promise<void> {
608
1077
  process.exit(0);
609
1078
  }
610
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
+ }
611
1097
  } else if (channel === "discord") {
612
1098
  p.note(
613
1099
  "1. Ve a https://discord.com/developers/applications\n" +
@@ -629,6 +1115,23 @@ export async function onboard(): Promise<void> {
629
1115
  }
630
1116
  }
631
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
+
632
1135
  const installService = await p.confirm({
633
1136
  message: "¿Instalar Hive como servicio del sistema? (arranca automáticamente)",
634
1137
  initialValue: false,
@@ -644,15 +1147,18 @@ export async function onboard(): Promise<void> {
644
1147
 
645
1148
  await generateConfig({
646
1149
  agentName: agentName as string,
1150
+ userName: userName as string,
1151
+ userId: userId,
647
1152
  provider: providerKey,
648
1153
  model: model as string,
649
1154
  apiKey,
650
1155
  channel: channel as string,
651
1156
  channelToken,
652
1157
  workspace: workspace as string,
1158
+ skills: selectedSkills as string[],
653
1159
  });
654
1160
 
655
- 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);
656
1162
 
657
1163
  spinner.stop("Configuración creada ✅");
658
1164
 
@@ -688,3 +1194,59 @@ export async function onboard(): Promise<void> {
688
1194
  ` hive agents add <nombre>`
689
1195
  );
690
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.5";
17
+ const VERSION = "1.0.7";
18
18
 
19
19
  const HELP = `
20
20
  🐝 Hive — Personal AI Gateway v${VERSION}