@johpaz/hive 1.1.0

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.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. package/packages/tools/tsconfig.json +9 -0
@@ -0,0 +1,1868 @@
1
+ import * as p from "@clack/prompts";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import {
5
+ INTEGRATIONS,
6
+ type Integration,
7
+ type Category,
8
+ } from "@johpaz/hive-core/integrations/catalog";
9
+ import { saveToken } from "@johpaz/hive-core/integrations/env";
10
+ import {
11
+ initOnboardingDb,
12
+ saveUserProfile,
13
+ saveProviderConfig,
14
+ saveAgentConfig,
15
+ saveOnboardingProgress,
16
+ activateChannel,
17
+ saveMcpServer,
18
+ activateEthics,
19
+ activateTools,
20
+ activateCodeBridge,
21
+ getAllEthics,
22
+ getAllCodeBridge,
23
+ getAllTools,
24
+ saveVoiceConfig,
25
+ } from "@johpaz/hive-core/storage/onboarding";
26
+ import { maskApiKey } from "@johpaz/hive-core/storage/crypto";
27
+ import { voiceService } from "@johpaz/hive-core/voice";
28
+ import { generateAuthToken } from "../utils/token";
29
+ import { getHiveDir } from "../../../core/src/config/loader";
30
+
31
+ // Log helper for @clack/prompts v0.5.1 (log export not available)
32
+ const log = {
33
+ success: (msg: string) => console.log(`\x1b[32m✔\x1b[0m ${msg}`),
34
+ error: (msg: string) => console.log(`\x1b[31m✖\x1b[0m ${msg}`),
35
+ warn: (msg: string) => console.log(`\x1b[33m⚠\x1b[0m ${msg}`),
36
+ info: (msg: string) => console.log(`\x1b[36mℹ\x1b[0m ${msg}`),
37
+ step: (msg: string) => console.log(`\n\x1b[36m›\x1b[0m ${msg}`),
38
+ };
39
+
40
+ function getEnvApiKey(keyName: string): string | null {
41
+ const hiveDir = getHiveDir();
42
+ const envPath = path.join(hiveDir, ".env");
43
+ if (!fs.existsSync(envPath)) return null;
44
+
45
+ const envContent = fs.readFileSync(envPath, "utf-8");
46
+ const match = envContent.match(new RegExp(`${keyName}=(.+)`));
47
+ return match ? match[1].trim() : null;
48
+ }
49
+
50
+ function reloadEnvToProcess(hiveDir: string): void {
51
+ const envPath = path.join(hiveDir, ".env");
52
+ if (!fs.existsSync(envPath)) return;
53
+
54
+ const envContent = fs.readFileSync(envPath, "utf-8");
55
+ const lines = envContent.split("\n");
56
+ for (const line of lines) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith("#")) continue;
59
+ const [key, ...valueParts] = trimmed.split("=");
60
+ if (key && valueParts.length > 0) {
61
+ process.env[key] = valueParts.join("=").trim();
62
+ }
63
+ }
64
+ }
65
+
66
+ const VERSION = "1.1.0";
67
+
68
+ const DEFAULT_MODELS: Record<string, string> = {
69
+ anthropic: "claude-sonnet-4-6",
70
+ openai: "gpt-5.2",
71
+ gemini: "gemini-3.1-pro-preview",
72
+ mistral: "mistral-large-latest",
73
+ deepseek: "deepseek-chat",
74
+ kimi: "kimi-k2.5",
75
+ openrouter: "meta-llama/llama-3.3-70b-instruct",
76
+ ollama: "llama3.3:8b",
77
+ };
78
+
79
+ const PROVIDER_BASE_URLS: Record<string, string> = {
80
+ anthropic: "https://api.anthropic.com",
81
+ openai: "https://api.openai.com",
82
+ gemini: "https://generativelanguage.googleapis.com",
83
+ mistral: "https://api.mistral.ai/v1",
84
+ deepseek: "https://api.deepseek.com",
85
+ kimi: "https://api.moonshot.cn",
86
+ openrouter: "https://openrouter.ai/api",
87
+ ollama: "http://localhost:11434",
88
+ };
89
+
90
+ const API_KEY_PLACEHOLDERS: Record<string, string> = {
91
+ anthropic: "sk-ant-...",
92
+ openai: "sk-...",
93
+ gemini: "AIza...",
94
+ mistral: "sk-...",
95
+ deepseek: "sk-...",
96
+ kimi: "sk-...",
97
+ openrouter: "sk-or-...",
98
+ ollama: "",
99
+ };
100
+
101
+ const API_KEY_LINKS: Record<string, string> = {
102
+ anthropic: "https://console.anthropic.com/keys",
103
+ openai: "https://platform.openai.com/api-keys",
104
+ gemini: "https://aistudio.google.com/app/apikey",
105
+ mistral: "https://console.mistral.ai/api-keys",
106
+ deepseek: "https://platform.deepseek.com/api_keys",
107
+ kimi: "https://platform.moonshot.cn/console/api-keys",
108
+ openrouter: "https://openrouter.ai/keys",
109
+ ollama: "",
110
+ };
111
+
112
+ const AVAILABLE_MODELS: Record<string, Array<{ value: string; label: string; hint?: string }>> = {
113
+ anthropic: [
114
+ { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Recomendado — mejor equilibrio, 1M contexto" },
115
+ { value: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Más potente — agentic coding, 1M contexto" },
116
+ { value: "claude-haiku-4-6", label: "Claude Haiku 4.6", hint: "Más rápido y económico" },
117
+ ],
118
+ openai: [
119
+ { value: "gpt-5.2", label: "GPT-5.2", hint: "Recomendado — 400K contexto, latest" },
120
+ { value: "gpt-5.1", label: "GPT-5.1", hint: "Versión anterior estable" },
121
+ { value: "gpt-5.2-codex", label: "GPT-5.2 Codex", hint: "Especializado en código" },
122
+ { value: "o4-mini", label: "o4-mini", hint: "Razonamiento avanzado, económico" },
123
+ ],
124
+ gemini: [
125
+ { value: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)", hint: "Frontier-class, muy económico" },
126
+ { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "Recomendado — estable, rápido" },
127
+ { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", hint: "Más potente — razonamiento profundo" },
128
+ { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)", hint: "Latest — tareas complejas" },
129
+ ],
130
+ mistral: [
131
+ { value: "mistral-large-latest", label: "Mistral Large", hint: "Recomendado — potente, 1M contexto" },
132
+ { value: "mistral-small-latest", label: "Mistral Small", hint: "Económico y rápido" },
133
+ { value: "pixtral-large-latest", label: "Pixtral Large", hint: "Multimodal — visión" },
134
+ ],
135
+ deepseek: [
136
+ { value: "deepseek-chat", label: "DeepSeek-V3", hint: "Recomendado — muy económico, capaz" },
137
+ { value: "deepseek-reasoner", label: "DeepSeek-R1", hint: "Razonamiento profundo" },
138
+ { value: "deepseek-coder", label: "DeepSeek Coder", hint: "Especializado en código" },
139
+ ],
140
+ kimi: [
141
+ { value: "kimi-k2.5", label: "Kimi K2.5", hint: "Recomendado — multimodal, agentic, 1T params" },
142
+ { value: "kimi-k2-thinking", label: "Kimi K2 Thinking", hint: "Largo razonamiento" },
143
+ { value: "kimi-k2-turbo-preview", label: "Kimi K2 Turbo", hint: "Rápido, preview" },
144
+ ],
145
+ openrouter: [
146
+ { value: "meta-llama/llama-3.3-70b-instruct", label: "Llama 3.3 70B", hint: "Gratis — GPT-4 level" },
147
+ { value: "google/gemini-2.0-flash-exp:free", label: "Gemini 2.0 Flash", hint: "Gratis — 1M contexto" },
148
+ { value: "deepseek/deepseek-r1:free", label: "DeepSeek R1", hint: "Gratis — razonamiento fuerte" },
149
+ { value: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Vía OpenRouter" },
150
+ ],
151
+ ollama: [
152
+ { value: "llama3.3:8b", label: "Llama 3.3 8B", hint: "Recomendado — general, ~5GB RAM" },
153
+ { value: "qwen2.5:7b", label: "Qwen 2.5 7B", hint: "Multilingual, código, ~4.5GB RAM" },
154
+ { value: "mistral:7b", label: "Mistral 7B", hint: "Rápido, ~4GB RAM" },
155
+ { value: "phi4:14b", label: "Phi-4 14B", hint: "Mejor calidad, ~8GB RAM" },
156
+ ],
157
+ };
158
+
159
+ const BUNDLED_SKILLS = [
160
+ { name: "web_search", label: "Web Search", hint: "Buscar en la web", default: true },
161
+ { name: "shell", label: "Shell", hint: "Ejecutar comandos", default: true },
162
+ { name: "file_manager", label: "File Manager", hint: "Operaciones de archivos", default: true },
163
+ { name: "http_client", label: "HTTP Client", hint: "Peticiones HTTP", default: true },
164
+ { name: "memory", label: "Memory", hint: "Memoria persistente", default: true },
165
+ { name: "cron_manager", label: "Cron Manager", hint: "Tareas programadas", default: false },
166
+ { name: "system_notify", label: "System Notify", hint: "Notificaciones desktop", default: false },
167
+ { name: "browser_automation", label: "Browser Automation", hint: "Automatizar navegador", default: false },
168
+ { name: "context_compact", label: "Context Compact", hint: "Compactar contexto", default: false },
169
+ ];
170
+
171
+ type NavAction = "next" | "prev" | "cancel";
172
+
173
+ async function askNavigation(): Promise<NavAction> {
174
+ const action = await p.select({
175
+ message: "Acciones:",
176
+ options: [
177
+ { value: "next", label: "➡️ Siguiente", hint: "Continuar" },
178
+ { value: "prev", label: "⬅️ Anterior", hint: "Volver atrás" },
179
+ { value: "cancel", label: "❌ Cancelar", hint: "Salir del onboarding" },
180
+ ],
181
+ });
182
+
183
+ if (p.isCancel(action) || action === "cancel") {
184
+ return "cancel";
185
+ }
186
+ return action as NavAction;
187
+ }
188
+
189
+ function showProgress(current: number, total: number, title: string): void {
190
+ const filled = "█".repeat(current);
191
+ const empty = "░".repeat(total - current);
192
+ p.note(`${filled}${empty} Sección ${current}/${total}: ${title}`, "Progreso");
193
+ }
194
+
195
+ interface OnboardConfig {
196
+ agentName: string;
197
+ agentId: string;
198
+ userName: string;
199
+ userId: string;
200
+ provider: string;
201
+ model: string;
202
+ apiKey: string;
203
+ channel: string;
204
+ channelToken: string;
205
+ workspace: string;
206
+ tools: string[];
207
+ mcp?: { servers: Record<string, unknown> };
208
+ agentTone: "formal" | "friendly" | "direct";
209
+
210
+ codeBridge: {
211
+ enabled: boolean;
212
+ clis: string[];
213
+ port: number;
214
+ };
215
+ userLanguage?: string;
216
+ userTimezone?: string;
217
+ userOccupation?: string;
218
+ userNotes?: string;
219
+ ethicsChoice?: string;
220
+ agentDescription: string;
221
+ }
222
+
223
+ async function checkCommand(cmd: string): Promise<boolean> {
224
+ const proc = Bun.spawn(["which", cmd], { stdout: "pipe", stderr: "pipe" })
225
+ const code = await proc.exited
226
+ return code === 0
227
+ }
228
+
229
+ function addMcpToConfig(integration: Integration, configObj: Record<string, any>): void {
230
+ configObj.mcp = configObj.mcp ?? {}
231
+ configObj.mcp.servers = configObj.mcp.servers ?? {}
232
+
233
+ const server: Record<string, unknown> = {
234
+ transport: integration.mcp.transport,
235
+ enabled: true,
236
+ builtin: true,
237
+ }
238
+
239
+ if (integration.mcp.transport === "stdio") {
240
+ server.command = integration.mcp.command
241
+ server.args = integration.mcp.args
242
+ if (integration.mcp.envVar) {
243
+ server.env = {
244
+ [integration.mcp.envVar]: `\${${integration.mcp.envVar}}`,
245
+ }
246
+ }
247
+ }
248
+
249
+ if (integration.mcp.transport === "sse") {
250
+ server.url = integration.mcp.url
251
+ }
252
+
253
+ configObj.mcp.servers[integration.id] = server
254
+ }
255
+
256
+ async function configureIntegration(
257
+ integration: Integration,
258
+ configObj: Record<string, unknown>
259
+ ): Promise<void> {
260
+ console.log()
261
+ log.step(`Configurando ${integration.name}`)
262
+
263
+ p.note(`¿Para qué sirve ${integration.name}?`, integration.example)
264
+
265
+ if (!integration.tokenValidation) {
266
+ p.note("Sin token requerido", integration.tokenInstructions)
267
+ addMcpToConfig(integration, configObj)
268
+ log.success(`${integration.name} añadido — autorizará cuando lo uses por primera vez`)
269
+ return
270
+ }
271
+
272
+ if (integration.requires) {
273
+ const cliAvailable = await checkCommand(integration.requires.command)
274
+ if (!cliAvailable) {
275
+ p.note(
276
+ `⚠️ Dependencia requerida para ${integration.name}`,
277
+ integration.requires.installHint
278
+ )
279
+ const continueAnyway = await p.confirm({
280
+ message: "¿Continuar de todas formas y configurar el token ahora?",
281
+ initialValue: false,
282
+ })
283
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
284
+ log.warn(`${integration.name} omitido — configúralo después con: hive integrations add`)
285
+ return
286
+ }
287
+ }
288
+ }
289
+
290
+ p.note(`¿Cómo obtener el token de ${integration.name}?`, integration.tokenInstructions)
291
+
292
+ const token = await p.password({
293
+ message: `Pega tu ${integration.mcp.envVarLabel}:`,
294
+ validate: (v) => {
295
+ if (!v?.trim()) return "El token no puede estar vacío"
296
+ if (integration.tokenFormat && !integration.tokenFormat.test(v.trim())) {
297
+ return `Formato incorrecto — revisa el token de ${integration.name}`
298
+ }
299
+ },
300
+ })
301
+
302
+ if (p.isCancel(token)) {
303
+ log.warn(`${integration.name} omitido`)
304
+ return
305
+ }
306
+
307
+ const spin = p.spinner()
308
+ spin.start(`Verificando conexión con ${integration.name}...`)
309
+
310
+ try {
311
+ const validation = integration.tokenValidation!
312
+ const method = validation.method ?? "GET"
313
+ const headers = validation.headers(token.trim())
314
+ const body = validation.body?.(token.trim())
315
+
316
+ const res = await fetch(validation.url, {
317
+ method,
318
+ headers,
319
+ ...(body ? { body } : {}),
320
+ signal: AbortSignal.timeout(10_000),
321
+ })
322
+
323
+ if (!res.ok) {
324
+ spin.stop(`Token inválido (${res.status}) — ${integration.name} no configurado ⚠️`)
325
+ log.warn(`Inténtalo después con: hive integrations add`)
326
+ return
327
+ }
328
+
329
+ const data = await res.json()
330
+ const username = validation.getUsername(data)
331
+ spin.stop(`${integration.name} conectado como ${username} ✅`)
332
+ } catch {
333
+ spin.stop(`Sin conexión para verificar — se guardará igualmente ⚠️`)
334
+ }
335
+
336
+ saveToken(integration.mcp.envVar!, token.trim())
337
+ addMcpToConfig(integration, configObj)
338
+ log.success(`${integration.name} listo`)
339
+ }
340
+
341
+ async function runIntegrationsSection(configObj: Record<string, unknown>): Promise<void> {
342
+ p.note(
343
+ "Integraciones de desarrollo (opcional)",
344
+ "Conecta Hive con servicios de desarrollo para que Bee pueda\n" +
345
+ "gestionar repositorios, publicar proyectos, manejar bases de\n" +
346
+ "datos y más. Puedes añadirlas ahora o después."
347
+ )
348
+
349
+ const wantsIntegrations = await p.confirm({
350
+ message: "¿Quieres configurar integraciones ahora?",
351
+ initialValue: true,
352
+ })
353
+
354
+ if (p.isCancel(wantsIntegrations)) return
355
+
356
+ if (!wantsIntegrations) {
357
+ log.info("Puedes añadirlas después con: hive integrations add")
358
+ return
359
+ }
360
+
361
+ const byCategory = new Map<Category, Integration[]>()
362
+ for (const i of INTEGRATIONS) {
363
+ const list = byCategory.get(i.category) ?? []
364
+ list.push(i)
365
+ byCategory.set(i.category, list)
366
+ }
367
+
368
+ const options: Array<{ value: string; label: string; hint?: string }> = []
369
+ for (const [category, integrations] of byCategory) {
370
+ for (const i of integrations) {
371
+ options.push({
372
+ value: i.id,
373
+ label: i.name,
374
+ hint: i.description,
375
+ })
376
+ }
377
+ }
378
+
379
+ const selected = await p.multiselect({
380
+ message: "Selecciona las que quieras conectar (Space = seleccionar, Enter = confirmar)",
381
+ options,
382
+ required: false,
383
+ })
384
+
385
+ if (p.isCancel(selected) || (selected as string[]).length === 0) {
386
+ log.info("No se seleccionaron integraciones")
387
+ return
388
+ }
389
+
390
+ for (const id of selected as string[]) {
391
+ const integration = INTEGRATIONS.find(i => i.id === id)
392
+ if (integration) {
393
+ await configureIntegration(integration, configObj)
394
+ }
395
+ }
396
+ }
397
+
398
+ function generateToken(): string {
399
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
400
+ let token = "";
401
+ for (let i = 0; i < 32; i++) {
402
+ token += chars.charAt(Math.floor(Math.random() * chars.length));
403
+ }
404
+ return token;
405
+ }
406
+
407
+ async function verifyTelegramToken(token: string): Promise<{ ok: boolean; username?: string }> {
408
+ try {
409
+ const res = await fetch(
410
+ `https://api.telegram.org/bot${token}/getMe`,
411
+ { signal: AbortSignal.timeout(5000) }
412
+ );
413
+ const data = (await res.json()) as {
414
+ ok: boolean;
415
+ result?: { username: string; first_name: string };
416
+ };
417
+ return {
418
+ ok: data.ok,
419
+ username: data.result?.username,
420
+ };
421
+ } catch {
422
+ return { ok: false };
423
+ }
424
+ }
425
+
426
+ function validateDiscordToken(token: string): boolean {
427
+ return token.length >= 50;
428
+ }
429
+
430
+ function validateSlackToken(token: string): boolean {
431
+ return token.startsWith("xoxb-") || token.startsWith("xoxp-");
432
+ }
433
+
434
+ async function testLLMConnection(provider: string, apiKey: string, model: string): Promise<boolean> {
435
+ if (provider === "ollama") {
436
+ try {
437
+ const response = await fetch("http://localhost:11434/api/tags");
438
+ return response.ok;
439
+ } catch {
440
+ return false;
441
+ }
442
+ }
443
+
444
+ const testMessages = [{ role: "user" as const, content: "Say 'ok' if you can read this." }];
445
+
446
+ try {
447
+ if (provider === "anthropic") {
448
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
449
+ method: "POST",
450
+ headers: {
451
+ "Content-Type": "application/json",
452
+ "x-api-key": apiKey,
453
+ "anthropic-version": "2023-06-01",
454
+ "anthropic-dangerous-direct-browser-access": "true",
455
+ },
456
+ body: JSON.stringify({
457
+ model: model,
458
+ max_tokens: 10,
459
+ messages: testMessages,
460
+ }),
461
+ });
462
+ return response.ok;
463
+ }
464
+
465
+ if (provider === "openai") {
466
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
467
+ method: "POST",
468
+ headers: {
469
+ "Content-Type": "application/json",
470
+ Authorization: `Bearer ${apiKey}`,
471
+ },
472
+ body: JSON.stringify({
473
+ model: model,
474
+ max_tokens: 10,
475
+ messages: testMessages,
476
+ }),
477
+ });
478
+ return response.ok;
479
+ }
480
+
481
+ if (provider === "gemini") {
482
+ const response = await fetch(
483
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
484
+ {
485
+ method: "POST",
486
+ headers: { "Content-Type": "application/json" },
487
+ body: JSON.stringify({
488
+ contents: [{ parts: [{ text: "Say ok" }] }],
489
+ }),
490
+ }
491
+ );
492
+ return response.ok;
493
+ }
494
+
495
+ if (provider === "deepseek") {
496
+ const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
497
+ method: "POST",
498
+ headers: {
499
+ "Content-Type": "application/json",
500
+ Authorization: `Bearer ${apiKey}`,
501
+ },
502
+ body: JSON.stringify({
503
+ model: model,
504
+ max_tokens: 10,
505
+ messages: testMessages,
506
+ }),
507
+ });
508
+ return response.ok;
509
+ }
510
+
511
+ if (provider === "kimi") {
512
+ const response = await fetch("https://api.moonshot.cn/v1/chat/completions", {
513
+ method: "POST",
514
+ headers: {
515
+ "Content-Type": "application/json",
516
+ Authorization: `Bearer ${apiKey}`,
517
+ },
518
+ body: JSON.stringify({
519
+ model: model,
520
+ max_tokens: 10,
521
+ messages: testMessages,
522
+ }),
523
+ });
524
+ return response.ok;
525
+ }
526
+
527
+ if (provider === "openrouter") {
528
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
529
+ method: "POST",
530
+ headers: {
531
+ "Content-Type": "application/json",
532
+ Authorization: `Bearer ${apiKey}`,
533
+ },
534
+ body: JSON.stringify({
535
+ model: model,
536
+ max_tokens: 10,
537
+ messages: testMessages,
538
+ }),
539
+ });
540
+ return response.ok;
541
+ }
542
+
543
+ return false;
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+
549
+ async function generateWorkspace(workspace: string, agentName: string, userName: string, userId: string, userLanguage: string, userTimezone: string, ethicsChoice: string): Promise<void> {
550
+ // Create workspace directory - .md files are now stored in SQLite
551
+ if (!fs.existsSync(workspace)) {
552
+ fs.mkdirSync(workspace, { recursive: true });
553
+ }
554
+
555
+ // Files are loaded from SQLite via the agent service
556
+ // Workspace directory is created but .md files are not generated here
557
+ // The system will use default prompts from the database
558
+ }
559
+
560
+ async function installSystemdService(): Promise<void> {
561
+ const home = process.env.HOME || "";
562
+ const systemdDir = path.join(home, ".config", "systemd", "user");
563
+
564
+ if (!fs.existsSync(systemdDir)) {
565
+ fs.mkdirSync(systemdDir, { recursive: true });
566
+ }
567
+
568
+ const serviceContent = `[Unit]
569
+ Description=Hive Personal AI Gateway
570
+ After=network-online.target
571
+ Wants=network-online.target
572
+
573
+ [Service]
574
+ Type=simple
575
+ ExecStart=${home}/.bun/bin/hive start
576
+ ExecStop=${home}/.bun/bin/hive stop
577
+ Restart=on-failure
578
+ RestartSec=5
579
+ Environment=PATH=${home}/.bun/bin:${home}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
580
+ WorkingDirectory=${home}
581
+
582
+ [Install]
583
+ WantedBy=default.target
584
+ `;
585
+
586
+ const servicePath = path.join(systemdDir, "hive.service");
587
+ fs.writeFileSync(servicePath, serviceContent, "utf-8");
588
+
589
+ const { spawnSync } = require("child_process");
590
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
591
+ spawnSync("systemctl", ["--user", "enable", "hive"], { stdio: "inherit" });
592
+ }
593
+
594
+ async function isGatewayRunning(): Promise<boolean> {
595
+ const pidFile = path.join(getHiveDir(), "gateway.pid");
596
+ if (!fs.existsSync(pidFile)) return false;
597
+
598
+ try {
599
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
600
+ process.kill(pid, 0);
601
+ return true;
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+
607
+ async function reloadGateway(): Promise<void> {
608
+ const pidFile = path.join(getHiveDir(), "gateway.pid");
609
+ if (!fs.existsSync(pidFile)) return;
610
+
611
+ try {
612
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
613
+ process.kill(pid, "SIGHUP");
614
+ } catch {
615
+ // Gateway not running
616
+ }
617
+ }
618
+
619
+ interface ExistingConfig {
620
+ agentName: string;
621
+ provider: string;
622
+ model: string;
623
+ channels: string[];
624
+ apiKey: string;
625
+ workspace: string;
626
+ skills: string[];
627
+ raw: Record<string, unknown>;
628
+ }
629
+
630
+ function parseExistingConfig(raw: Record<string, unknown>): ExistingConfig {
631
+ const agents = (raw.agents as Record<string, unknown>)?.list as Array<Record<string, unknown>> | undefined;
632
+ const models = raw.models as Record<string, unknown> | undefined;
633
+ const providers = models?.providers as Record<string, Record<string, unknown>> | undefined;
634
+ const defaultProvider = models?.defaultProvider as string | undefined;
635
+ const providerConfig = providers?.[defaultProvider || ""] as Record<string, unknown> | undefined;
636
+
637
+ return {
638
+ agentName: (agents?.[0]?.name as string) ?? "Hive",
639
+ provider: defaultProvider ?? "gemini",
640
+ model: (models?.defaults as Record<string, string>)?.[defaultProvider || ""] ?? "",
641
+ channels: Object.keys((raw.channels as Record<string, unknown>) ?? {}),
642
+ apiKey: (providerConfig?.apiKey as string) ?? "",
643
+ workspace: (agents?.[0]?.workspace as string) ?? path.join(getHiveDir(), "workspace"),
644
+ skills: ((raw.skills as Record<string, unknown>)?.allowBundled as string[]) ?? [],
645
+ raw,
646
+ };
647
+ }
648
+
649
+ async function runUpdateWizard(existing: ExistingConfig): Promise<void> {
650
+ p.note(
651
+ "Presiona Enter para mantener el valor actual de cada campo.",
652
+ "✏️ Modo actualización"
653
+ );
654
+
655
+ // Nombre del agente
656
+ const agentName = await p.text({
657
+ message: "Nombre del agente:",
658
+ placeholder: existing.agentName,
659
+ defaultValue: existing.agentName,
660
+ });
661
+
662
+ if (p.isCancel(agentName)) {
663
+ p.cancel("Actualización cancelada.");
664
+ process.exit(0);
665
+ }
666
+
667
+ // Proveedor LLM
668
+ const changeProvider = await p.confirm({
669
+ message: `Proveedor actual: ${existing.provider} (${existing.model || "no configurado"}). ¿Cambiar?`,
670
+ initialValue: false,
671
+ });
672
+
673
+ if (p.isCancel(changeProvider)) {
674
+ p.cancel("Actualización cancelada.");
675
+ process.exit(0);
676
+ }
677
+
678
+ let provider = existing.provider;
679
+ let model = existing.model;
680
+ let apiKey = existing.apiKey;
681
+
682
+ if (changeProvider) {
683
+ provider = await p.select({
684
+ message: "Nuevo proveedor LLM:",
685
+ options: [
686
+ { value: "anthropic", label: "Anthropic (Claude)", hint: "Recomendado — Claude 4.6" },
687
+ { value: "openai", label: "OpenAI (GPT-5)", hint: "GPT-5.2" },
688
+ { value: "gemini", label: "Google Gemini", hint: "Gemini 3 Flash" },
689
+ { value: "mistral", label: "Mistral AI", hint: "Mistral Large" },
690
+ { value: "deepseek", label: "DeepSeek", hint: "Muy económico" },
691
+ { value: "kimi", label: "Kimi (Moonshot AI)", hint: "Contexto largo" },
692
+ { value: "openrouter", label: "OpenRouter", hint: "Multi-modelo" },
693
+ { value: "ollama", label: "Ollama (local)", hint: "Sin costo" },
694
+ ],
695
+ }) as string;
696
+
697
+ if (p.isCancel(provider)) {
698
+ p.cancel("Actualización cancelada.");
699
+ process.exit(0);
700
+ }
701
+
702
+ const models = AVAILABLE_MODELS[provider] || [{ value: DEFAULT_MODELS[provider], label: DEFAULT_MODELS[provider] }];
703
+
704
+ if (models.length > 1) {
705
+ model = await p.select({
706
+ message: `Modelo de ${provider}:`,
707
+ options: models,
708
+ }) as string;
709
+
710
+ if (p.isCancel(model)) {
711
+ p.cancel("Actualización cancelada.");
712
+ process.exit(0);
713
+ }
714
+ } else {
715
+ model = models[0].value;
716
+ }
717
+
718
+ if (provider !== "ollama") {
719
+ const link = API_KEY_LINKS[provider];
720
+ if (link) {
721
+ p.note(`Obtén tu API key en:\n${link}`, `API key de ${provider}`);
722
+ }
723
+
724
+ const keyResult = await p.password({
725
+ message: `API key de ${provider}:`,
726
+ validate: (v) => (!v || v.length < 10 ? "La key parece muy corta" : undefined),
727
+ });
728
+
729
+ if (p.isCancel(keyResult)) {
730
+ p.cancel("Actualización cancelada.");
731
+ process.exit(0);
732
+ }
733
+ apiKey = keyResult;
734
+
735
+ const spinner = p.spinner();
736
+ spinner.start(`Verificando conexión con ${provider}...`);
737
+
738
+ const connected = await testLLMConnection(provider, apiKey, model as string);
739
+
740
+ if (!connected) {
741
+ spinner.stop(`❌ Error conectando con ${provider}`);
742
+ p.outro("API key inválida. Ejecuta 'hive onboard' de nuevo con la key correcta.");
743
+ process.exit(1);
744
+ }
745
+
746
+ spinner.stop(`✅ Conexión con ${provider} verificada`);
747
+ }
748
+ }
749
+
750
+ // Canales
751
+ const currentChannels = existing.channels.length > 0 ? existing.channels.join(", ") : "ninguno";
752
+ const changeChannel = await p.confirm({
753
+ message: `Canales configurados: ${currentChannels}. ¿Añadir o cambiar?`,
754
+ initialValue: false,
755
+ });
756
+
757
+ if (p.isCancel(changeChannel)) {
758
+ p.cancel("Actualización cancelada.");
759
+ process.exit(0);
760
+ }
761
+
762
+ let channels = existing.raw.channels as Record<string, unknown> | undefined;
763
+
764
+ if (changeChannel) {
765
+ const channel = await p.select({
766
+ message: "Canal a configurar:",
767
+ options: [
768
+ { value: "telegram", label: "Telegram", hint: "Recomendado" },
769
+ { value: "discord", label: "Discord" },
770
+ { value: "webchat", label: "WebChat (local)" },
771
+ { value: "none", label: "Ninguno" },
772
+ ],
773
+ }) as string;
774
+
775
+ if (p.isCancel(channel)) {
776
+ p.cancel("Actualización cancelada.");
777
+ process.exit(0);
778
+ }
779
+
780
+ if (channel === "telegram") {
781
+ p.note(
782
+ "1. Abre Telegram y busca @BotFather\n" +
783
+ "2. Escribe /newbot y sigue las instrucciones\n" +
784
+ "3. Copia el token que te da BotFather",
785
+ "Cómo obtener el token de Telegram"
786
+ );
787
+
788
+ const tokenResult = await p.password({
789
+ message: "Token de Telegram BotFather:",
790
+ validate: (v) => (!v?.trim() ? "El token no puede estar vacío" : undefined),
791
+ });
792
+
793
+ if (p.isCancel(tokenResult)) {
794
+ p.cancel("Actualización cancelada.");
795
+ process.exit(0);
796
+ }
797
+
798
+ const spinner = p.spinner();
799
+ spinner.start("Verificando token de Telegram...");
800
+ const tg = await verifyTelegramToken(tokenResult as string);
801
+ if (tg.ok && tg.username) {
802
+ spinner.stop(`✅ Bot verificado: @${tg.username}`);
803
+ } else {
804
+ spinner.stop("⚠️ Token no verificado — se guardará de todas formas");
805
+ p.note(
806
+ "El token se guardó pero no pudo verificarse.\n" +
807
+ "Si es incorrecto, ejecuta: hive onboard (opción actualizar)",
808
+ "Aviso"
809
+ );
810
+ }
811
+
812
+ const telegramAccount: Record<string, unknown> = {
813
+ botToken: tokenResult,
814
+ dmPolicy: "open",
815
+ };
816
+
817
+ channels = {
818
+ telegram: {
819
+ accounts: {
820
+ default: telegramAccount,
821
+ },
822
+ },
823
+ };
824
+ } else if (channel === "discord") {
825
+ p.note(
826
+ "1. Ve a https://discord.com/developers/applications\n" +
827
+ "2. Crea una nueva aplicación\n" +
828
+ "3. Ve a Bot → Reset Token\n" +
829
+ "4. Habilita 'Message Content Intent'",
830
+ "Cómo obtener el token de Discord"
831
+ );
832
+
833
+ const tokenResult = await p.password({
834
+ message: "Token del bot de Discord:",
835
+ validate: (v) => (!v?.trim() ? "El token no puede estar vacío" : undefined),
836
+ });
837
+
838
+ if (p.isCancel(tokenResult)) {
839
+ p.cancel("Actualización cancelada.");
840
+ process.exit(0);
841
+ }
842
+
843
+ channels = {
844
+ discord: {
845
+ accounts: {
846
+ default: {
847
+ token: tokenResult,
848
+ },
849
+ },
850
+ },
851
+ };
852
+ }
853
+ }
854
+
855
+ // Guardar cambios
856
+ const spinner = p.spinner();
857
+ spinner.start("Guardando cambios...");
858
+
859
+ const baseUrlMap: Record<string, string> = {
860
+ gemini: "https://generativelanguage.googleapis.com/v1beta",
861
+ deepseek: "https://api.deepseek.com/v1",
862
+ kimi: "https://api.moonshot.cn/v1",
863
+ ollama: "http://localhost:11434/api",
864
+ };
865
+
866
+ const providersConfig: Record<string, Record<string, unknown>> = {};
867
+ if (provider !== "ollama" && apiKey) {
868
+ providersConfig[provider] = { apiKey };
869
+ if (baseUrlMap[provider]) {
870
+ providersConfig[provider].baseUrl = baseUrlMap[provider];
871
+ }
872
+ }
873
+
874
+ const updatedConfig: Record<string, unknown> = {
875
+ ...existing.raw,
876
+ name: agentName,
877
+ agents: {
878
+ list: [
879
+ {
880
+ id: "main",
881
+ default: true,
882
+ name: agentName,
883
+ workspace: existing.workspace,
884
+ agentDir: path.join(getHiveDir(), "agents", "main", "agent"),
885
+ },
886
+ ],
887
+ },
888
+ models: {
889
+ defaultProvider: provider,
890
+ defaults: {
891
+ [provider]: model,
892
+ },
893
+ providers: providersConfig,
894
+ },
895
+ };
896
+
897
+ if (channels) {
898
+ updatedConfig.channels = channels;
899
+ }
900
+
901
+ log.success("✅ Configuración actualizada en BD");
902
+ // Also update SQLite
903
+ try {
904
+ const { initializeDatabase, getDb } = await import("../../../core/src/storage/sqlite");
905
+ initializeDatabase();
906
+ const sqliteDb = getDb();
907
+
908
+ const apiKeyVal = (providersConfig[provider]?.apiKey as string) || null;
909
+ const baseUrlVal = (providersConfig[provider]?.baseUrl as string) || null;
910
+
911
+ sqliteDb.query(`
912
+ UPDATE providers SET api_key = ?, base_url = ?
913
+ WHERE id = ?
914
+ `).run(apiKeyVal, baseUrlVal, provider);
915
+
916
+ sqliteDb.query(`
917
+ UPDATE agents SET name = ?, provider_id = ?, model_id = ?
918
+ WHERE id = 'main'
919
+ `).run(agentName, provider, model);
920
+
921
+ console.log("✅ SQLite actualizado");
922
+ } catch (error) {
923
+ console.log("⚠️ Error actualizando SQLite:", (error as Error).message);
924
+ }
925
+
926
+ spinner.stop("Cambios guardados ✅");
927
+
928
+ // Recargar Gateway si está corriendo
929
+ const running = await isGatewayRunning();
930
+ if (running) {
931
+ const reload = await p.confirm({
932
+ message: "El Gateway está corriendo. ¿Recargar configuración ahora?",
933
+ initialValue: true,
934
+ });
935
+
936
+ if (reload) {
937
+ await reloadGateway();
938
+ log.success("Configuración recargada ✅");
939
+ }
940
+ }
941
+
942
+ const channelDisplay = channels ? Object.keys(channels).join(", ") || "ninguno" : existing.channels.join(", ") || "ninguno";
943
+
944
+ p.outro(
945
+ `✅ Configuración actualizada.\n\n` +
946
+ ` Agente: ${agentName}\n` +
947
+ ` Proveedor: ${provider} (${model})\n` +
948
+ ` Canales: ${channelDisplay}\n\n` +
949
+ ` hive status → ver estado actual\n` +
950
+ ` hive reload → recargar manualmente`
951
+ );
952
+ }
953
+
954
+ async function runFullWizard(): Promise<void> {
955
+ p.intro("🐝 Bienvenido a Hive — Personal AI Gateway");
956
+
957
+ // Initialize DB at start
958
+ initOnboardingDb();
959
+
960
+ const TOTAL_STEPS = 8;
961
+ let step = 1;
962
+
963
+ // Shared wizard state — pre-populated with defaults
964
+ const state = {
965
+ // Step 1: User Profile
966
+ agentName: "Hive",
967
+ userName: "Usuario",
968
+ userId: "", // Se genera automáticamente en la BD
969
+ userLanguage: "Spanish",
970
+ userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "America/Bogota",
971
+ userOccupation: "",
972
+ userNotes: "",
973
+ sessionToken: "", // For webchat user_identity
974
+ // Step 2: Agent Profile
975
+ agentId: "", // Se genera automáticamente en la BD
976
+ agentTone: "friendly" as "formal" | "friendly" | "direct",
977
+ agentDescription: "Asistente personal inteligente y desarrollador senior.",
978
+ // Step 3: Ethics
979
+ ethicsId: "default",
980
+ // Step 4: Tools
981
+ tools: [] as string[],
982
+ // Step 5: Provider
983
+ provider: "gemini",
984
+ model: "gemini-2.5-flash",
985
+ apiKey: "",
986
+ // Voice config
987
+ voiceEnabled: false,
988
+ sttProvider: "",
989
+ ttsProvider: "",
990
+ ttsVoiceId: "",
991
+ elevenlabsApiKey: "",
992
+ // Step 6: Channel
993
+ channel: "webchat",
994
+ channelToken: "",
995
+ // Step 7: Code Bridge
996
+ codeBridgeEnabled: false,
997
+ codeBridgeClis: [] as string[],
998
+ };
999
+
1000
+ const hiveDir = getHiveDir();
1001
+
1002
+ // ─────────────────────────────────────
1003
+ // Step machine: 9 steps in correct order
1004
+ // ─────────────────────────────────────
1005
+
1006
+ while (step <= TOTAL_STEPS) {
1007
+ switch (step) {
1008
+
1009
+ // ═══════════════════════════════════
1010
+ // STEP 1: User Profile
1011
+ // ═══════════════════════════════════
1012
+ case 1: {
1013
+ showProgress(step, TOTAL_STEPS, "Tu perfil");
1014
+
1015
+ const userName = await p.text({
1016
+ message: "👤 ¿Cómo te llamas?",
1017
+ placeholder: "Usuario",
1018
+ defaultValue: state.userName,
1019
+ });
1020
+ if (p.isCancel(userName)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1021
+ state.userName = userName;
1022
+
1023
+ const userLanguage = await p.select({
1024
+ message: "🌐 ¿En qué idioma prefieres que te responda?",
1025
+ options: [
1026
+ { value: "Spanish", label: "Español" },
1027
+ { value: "English", label: "English" },
1028
+ { value: "Spanish, English", label: "Ambos / Both" },
1029
+ ],
1030
+ initialValue: state.userLanguage,
1031
+ });
1032
+ if (p.isCancel(userLanguage)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1033
+ state.userLanguage = userLanguage as string;
1034
+
1035
+ const userTimezone = await p.text({
1036
+ message: "🕐 ¿Cuál es tu zona horaria?",
1037
+ placeholder: "America/Bogota",
1038
+ defaultValue: state.userTimezone,
1039
+ });
1040
+ if (p.isCancel(userTimezone)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1041
+ state.userTimezone = userTimezone as string;
1042
+
1043
+ const userOccupation = await p.text({
1044
+ message: "💼 ¿A qué te dedicas?",
1045
+ placeholder: "desarrollador de software",
1046
+ defaultValue: state.userOccupation || undefined,
1047
+ });
1048
+ if (p.isCancel(userOccupation)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1049
+ state.userOccupation = userOccupation || "";
1050
+
1051
+ const userNotes = await p.text({
1052
+ message: "💡 ¿Algo importante que el agente deba recordar de ti?",
1053
+ placeholder: "Prefiero respuestas directas.",
1054
+ defaultValue: state.userNotes || undefined,
1055
+ });
1056
+ if (p.isCancel(userNotes)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1057
+ state.userNotes = userNotes || "";
1058
+
1059
+ // Generate session token for webchat (user_identity)
1060
+ state.sessionToken = generateAuthToken();
1061
+
1062
+ // ✅ Save user to DB (agent will be saved in Step 2)
1063
+ // userId se genera automáticamente en la BD (randomblob)
1064
+ state.userId = saveUserProfile({
1065
+ userName: state.userName,
1066
+ userLanguage: state.userLanguage,
1067
+ userTimezone: state.userTimezone,
1068
+ userOccupation: state.userOccupation,
1069
+ userNotes: state.userNotes,
1070
+ channelUserId: state.sessionToken, // For webchat user_identity
1071
+ });
1072
+
1073
+ saveOnboardingProgress({
1074
+ step: "user",
1075
+ userId: state.userId,
1076
+ data: { userName: state.userName, userLanguage: state.userLanguage, userTimezone: state.userTimezone, userOccupation: state.userOccupation, userNotes: state.userNotes },
1077
+ });
1078
+
1079
+ log.success(`✅ Usuario guardado con ID: ${state.userId}`);
1080
+
1081
+ const nav = await askNavigation();
1082
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1083
+ if (nav === "prev") { step = 1; break; }
1084
+ step = 2;
1085
+ break;
1086
+ }
1087
+
1088
+ // ═══════════════════════════════════
1089
+ // STEP 2: Perfil del Agente
1090
+ // ═══════════════════════════════════
1091
+ case 2: {
1092
+ showProgress(step, TOTAL_STEPS, "Tu agente");
1093
+
1094
+ const agentName = await p.text({
1095
+ message: "🤖 ¿Cómo se llamará tu agente?",
1096
+ placeholder: "Bee",
1097
+ defaultValue: state.agentName,
1098
+ });
1099
+ if (p.isCancel(agentName)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1100
+ state.agentName = agentName;
1101
+
1102
+ const agentDescription = await p.text({
1103
+ message: "📝 ¿Qué quieres que haga este agente? (Su objetivo principal)",
1104
+ placeholder: "Ser un asistente personal inteligente y experto en desarrollo.",
1105
+ defaultValue: state.agentDescription,
1106
+ });
1107
+ if (p.isCancel(agentDescription)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1108
+ state.agentDescription = agentDescription;
1109
+
1110
+ const agentTone = await p.select({
1111
+ message: "🎭 Elige el tono de comunicación del agente:",
1112
+ options: [
1113
+ { value: "friendly", label: "Amigable y cercano" },
1114
+ { value: "professional", label: "Profesional y formal" },
1115
+ { value: "direct", label: "Directo y conciso" },
1116
+ { value: "casual", label: "Casual y relajado" },
1117
+ ],
1118
+ initialValue: state.agentTone,
1119
+ });
1120
+ if (p.isCancel(agentTone)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1121
+ state.agentTone = agentTone as "formal" | "friendly" | "direct";
1122
+
1123
+ // ✅ Save agent to DB (agentId se genera automáticamente)
1124
+ state.agentId = saveAgentConfig({
1125
+ userId: state.userId,
1126
+ agentName: state.agentName,
1127
+ description: state.agentDescription,
1128
+ tone: state.agentTone,
1129
+ providerId: "",
1130
+ modelId: "",
1131
+ });
1132
+
1133
+ // Activate default ethics
1134
+ activateEthics(state.userId, "default");
1135
+
1136
+ saveOnboardingProgress({
1137
+ step: "agent",
1138
+ userId: state.userId,
1139
+ data: { agentName: state.agentName, agentDescription: state.agentDescription, agentTone: state.agentTone, agentId: state.agentId },
1140
+ });
1141
+
1142
+ log.success(`✅ Agente creado con ID: ${state.agentId}`);
1143
+
1144
+ const nav = await askNavigation();
1145
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1146
+ if (nav === "prev") { step = 1; break; }
1147
+ step = 3;
1148
+ break;
1149
+ }
1150
+
1151
+ // ═══════════════════════════════════
1152
+ // STEP 3: Ethics
1153
+ // ═══════════════════════════════════
1154
+ case 3: {
1155
+ showProgress(step, TOTAL_STEPS, "Ética");
1156
+
1157
+ p.note("Los lineamientos éticos tienen MÁXIMA prioridad.", "Ética del agente");
1158
+
1159
+ const ethicsOptions = getAllEthics();
1160
+ console.log("📋 Ethics options:", ethicsOptions);
1161
+
1162
+ if (ethicsOptions.length === 0) {
1163
+ log.warn("No hay éticas disponibles en la BD. Usando ética por defecto.");
1164
+ state.ethicsId = "default";
1165
+ } else {
1166
+ const selectedEthics = await p.select({
1167
+ message: "¿Qué lineamientos éticos prefieres?",
1168
+ options: ethicsOptions.map(e => ({
1169
+ value: e.id,
1170
+ label: e.name,
1171
+ hint: e.isDefault ? "Recomendado" : e.description,
1172
+ })),
1173
+ initialValue: state.ethicsId,
1174
+ });
1175
+ if (p.isCancel(selectedEthics)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1176
+ state.ethicsId = selectedEthics as string;
1177
+ }
1178
+
1179
+ // ✅ Activate ethics in DB
1180
+ activateEthics(state.userId, state.ethicsId);
1181
+
1182
+ saveOnboardingProgress({
1183
+ step: "ethics",
1184
+ userId: state.userId,
1185
+ data: { ethicsId: state.ethicsId },
1186
+ });
1187
+
1188
+ const nav = await askNavigation();
1189
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1190
+ if (nav === "prev") { step = 2; break; }
1191
+ step = 4;
1192
+ break;
1193
+ }
1194
+
1195
+ // ═══════════════════════════════════
1196
+ // STEP 4: Provider & Model
1197
+ // ═══════════════════════════════════
1198
+ case 4: {
1199
+ showProgress(step, TOTAL_STEPS, "Proveedor LLM");
1200
+
1201
+ const provider = await p.select({
1202
+ message: "¿Qué proveedor de LLM quieres usar?",
1203
+ options: [
1204
+ { value: "gemini", label: "Google Gemini", hint: "Recomendado — económico, gran contexto" },
1205
+ { value: "anthropic", label: "Anthropic Claude", hint: "Premium — mejor calidad" },
1206
+ { value: "openai", label: "OpenAI", hint: "Estándar de la industria" },
1207
+ { value: "ollama", label: "Ollama (Local)", hint: "Gratis, corre localmente" },
1208
+ { value: "openrouter", label: "OpenRouter", hint: "Múltiples proveedores" },
1209
+ ],
1210
+ initialValue: state.provider,
1211
+ });
1212
+ if (p.isCancel(provider)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1213
+ state.provider = provider as string;
1214
+
1215
+ const models = AVAILABLE_MODELS[state.provider] || [];
1216
+ const model = await p.select({
1217
+ message: `¿Qué modelo de ${provider}?`,
1218
+ options: models,
1219
+ initialValue: DEFAULT_MODELS[state.provider],
1220
+ });
1221
+ if (p.isCancel(model)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1222
+ state.model = model as string;
1223
+
1224
+ let apiKey = "";
1225
+ if (state.provider !== "ollama") {
1226
+ const link = API_KEY_LINKS[state.provider];
1227
+ if (link) {
1228
+ p.note(`Obtén tu API key en: ${link}`, "API Key");
1229
+ }
1230
+ const keyResult = await p.password({
1231
+ message: `API key de ${state.provider}:`,
1232
+ validate: (v) => (!v || v.length < 10 ? "La key parece muy corta" : undefined),
1233
+ });
1234
+ if (p.isCancel(keyResult)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1235
+ apiKey = keyResult;
1236
+ }
1237
+ state.apiKey = apiKey;
1238
+
1239
+ // ✅ Save provider & activate model in DB
1240
+ await saveProviderConfig({
1241
+ userId: state.userId,
1242
+ provider: state.provider,
1243
+ model: state.model,
1244
+ apiKey: state.apiKey,
1245
+ baseUrl: PROVIDER_BASE_URLS[state.provider],
1246
+ });
1247
+
1248
+ saveOnboardingProgress({
1249
+ step: "provider",
1250
+ userId: state.userId,
1251
+ data: { provider: state.provider, model: state.model },
1252
+ });
1253
+
1254
+ const nav = await askNavigation();
1255
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1256
+ if (nav === "prev") { step = 3; break; }
1257
+ step = 5;
1258
+ break;
1259
+ }
1260
+
1261
+ // ═══════════════════════════════════
1262
+ // STEP 5: Voice Configuration
1263
+ // ═══════════════════════════════════
1264
+ case 5: {
1265
+ showProgress(step, TOTAL_STEPS, "Voz");
1266
+
1267
+ p.note("Configura el reconocimiento de voz y síntesis de audio.", "Voz");
1268
+
1269
+ const wantsVoice = await p.confirm({
1270
+ message: "¿Quieres habilitar voz en tu agente?",
1271
+ initialValue: false,
1272
+ });
1273
+ if (p.isCancel(wantsVoice)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1274
+
1275
+ state.voiceEnabled = wantsVoice as boolean;
1276
+
1277
+ if (wantsVoice) {
1278
+ // Variables to store API keys for saving to DB later
1279
+ let sttApiKeyValue: string | null = null;
1280
+ let ttsApiKeyValue: string | null = null;
1281
+
1282
+ // Ask for STT provider (speech-to-text)
1283
+ const sttProvider = await p.select({
1284
+ message: "¿Qué servicio de transcripción de voz (STT)?",
1285
+ options: [
1286
+ { value: "whisper-large-v3-turbo", label: "Groq Whisper Large V3 Turbo", hint: "Recomendado - rápido" },
1287
+ { value: "whisper-large-v3", label: "Groq Whisper Large V3", hint: "Máxima precisión" },
1288
+ { value: "distil-whisper-large-v3-en", label: "Groq Distil Whisper Large V3", hint: "Solo inglés" },
1289
+ { value: "whisper-1", label: "OpenAI Whisper 1", hint: "Alternativa" },
1290
+ ],
1291
+ initialValue: state.sttProvider || "whisper-large-v3-turbo",
1292
+ });
1293
+ if (p.isCancel(sttProvider)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1294
+ state.sttProvider = sttProvider as string;
1295
+
1296
+ // Ask for API key based on STT provider
1297
+ const isGroqStt = ["whisper-large-v3", "whisper-large-v3-turbo", "distil-whisper-large-v3-en"].includes(sttProvider);
1298
+ const isOpenAiStt = sttProvider === "whisper-1";
1299
+
1300
+ if (isGroqStt) {
1301
+ const existingGroqKey = getEnvApiKey("GROQ_API_KEY");
1302
+ let groqKey: string | null = existingGroqKey;
1303
+
1304
+ if (!groqKey) {
1305
+ const groqKeyInput = await p.password({
1306
+ message: "Ingresa tu GROQ_API_KEY:",
1307
+ validate: (value) => {
1308
+ if (!value || value.length < 10) return "API key inválida";
1309
+ },
1310
+ });
1311
+ if (p.isCancel(groqKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1312
+ groqKey = groqKeyInput as string;
1313
+ sttApiKeyValue = groqKey;
1314
+
1315
+ // Save Groq API key to .env
1316
+ const hiveDir = getHiveDir();
1317
+ const envPath = path.join(hiveDir, ".env");
1318
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1319
+ const newEnv = envContent.includes("GROQ_API_KEY")
1320
+ ? envContent.replace(/GROQ_API_KEY=.*/g, `GROQ_API_KEY=${groqKey}`)
1321
+ : envContent + `\nGROQ_API_KEY=${groqKey}`;
1322
+ fs.writeFileSync(envPath, newEnv);
1323
+ reloadEnvToProcess(hiveDir);
1324
+ log.success("✅ API key de Groq guardada en .env");
1325
+ } else {
1326
+ log.info("✅ GROQ_API_KEY ya configurada en .env");
1327
+ sttApiKeyValue = groqKey;
1328
+ }
1329
+ } else if (isOpenAiStt) {
1330
+ const existingOpenAiKey = getEnvApiKey("OPENAI_API_KEY");
1331
+ let openaiKey: string | null = existingOpenAiKey;
1332
+
1333
+ if (!openaiKey) {
1334
+ const openaiKeyInput = await p.password({
1335
+ message: "Ingresa tu OPENAI_API_KEY:",
1336
+ validate: (value) => {
1337
+ if (!value || value.length < 10) return "API key inválida";
1338
+ },
1339
+ });
1340
+ if (p.isCancel(openaiKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1341
+ openaiKey = openaiKeyInput as string;
1342
+ sttApiKeyValue = openaiKey;
1343
+
1344
+ // Save OpenAI API key to .env
1345
+ const hiveDir = getHiveDir();
1346
+ const envPath = path.join(hiveDir, ".env");
1347
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1348
+ const newEnv = envContent.includes("OPENAI_API_KEY")
1349
+ ? envContent.replace(/OPENAI_API_KEY=.*/g, `OPENAI_API_KEY=${openaiKey}`)
1350
+ : envContent + `\nOPENAI_API_KEY=${openaiKey}`;
1351
+ fs.writeFileSync(envPath, newEnv);
1352
+ reloadEnvToProcess(hiveDir);
1353
+ log.success("✅ API key de OpenAI guardada en .env");
1354
+ } else {
1355
+ log.info("✅ OPENAI_API_KEY ya configurada en .env");
1356
+ sttApiKeyValue = openaiKey;
1357
+ }
1358
+ }
1359
+
1360
+ // Ask for TTS provider (text-to-speech)
1361
+ const ttsProvider = await p.select({
1362
+ message: "¿Qué servicio de síntesis de voz (TTS)?",
1363
+ options: [
1364
+ { value: "eleven_flash_v2_5", label: "ElevenLabs Flash V2.5", hint: "Tiempo real - recomendado" },
1365
+ { value: "eleven_turbo_v2_5", label: "ElevenLabs Turbo V2.5", hint: "Balance calidad/velocidad" },
1366
+ { value: "eleven_multilingual_v2", label: "ElevenLabs Multilingual V2", hint: "Alta calidad" },
1367
+ { value: "eleven_v3", label: "ElevenLabs V3", hint: "Más expresivo" },
1368
+ { value: "qwen3-tts-instruct-flash", label: "Qwen TTS Flash", hint: "Gratis - recomendado" },
1369
+ { value: "gpt-4o-mini-tts", label: "OpenAI GPT-4o Mini TTS", hint: "Usa tu API key" },
1370
+ { value: "tts-1", label: "OpenAI TTS-1", hint: "Estándar" },
1371
+ { value: "tts-1-hd", label: "OpenAI TTS-1 HD", hint: "Alta calidad" },
1372
+ { value: "gemini-2.5-flash-preview-tts", label: "Gemini 2.5 Flash TTS", hint: "Google" },
1373
+ { value: "gemini-2.5-pro-preview-tts", label: "Gemini 2.5 Pro TTS", hint: "Google Pro" },
1374
+ ],
1375
+ initialValue: state.ttsProvider || "eleven_flash_v2_5",
1376
+ });
1377
+ if (p.isCancel(ttsProvider)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1378
+ state.ttsProvider = ttsProvider as string;
1379
+
1380
+ // Ask for API key based on TTS provider
1381
+ const isElevenLabs = ["eleven_flash_v2_5", "eleven_turbo_v2_5", "eleven_multilingual_v2", "eleven_v3"].includes(ttsProvider);
1382
+ const isOpenAI = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"].includes(ttsProvider);
1383
+ const isGemini = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"].includes(ttsProvider);
1384
+ const isQwen = ["qwen3-tts-instruct-flash", "qwen3-tts-flash", "qwen-tts"].includes(ttsProvider);
1385
+
1386
+ if (isElevenLabs) {
1387
+ const existingElevenKey = getEnvApiKey("ELEVENLABS_API_KEY");
1388
+ let elevenKey: string | null = existingElevenKey;
1389
+
1390
+ if (!elevenKey) {
1391
+ const elevenKeyInput = await p.password({
1392
+ message: "Ingresa tu ELEVENLABS_API_KEY:",
1393
+ validate: (value) => {
1394
+ if (!value || value.length < 10) return "API key inválida";
1395
+ },
1396
+ });
1397
+ if (p.isCancel(elevenKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1398
+ elevenKey = elevenKeyInput as string;
1399
+ state.elevenlabsApiKey = elevenKey;
1400
+
1401
+ // Save to .env
1402
+ const hiveDir = getHiveDir();
1403
+ const envPath = path.join(hiveDir, ".env");
1404
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1405
+ const newEnv = envContent.includes("ELEVENLABS_API_KEY")
1406
+ ? envContent.replace(/ELEVENLABS_API_KEY=.*/g, `ELEVENLABS_API_KEY=${elevenKey}`)
1407
+ : envContent + `\nELEVENLABS_API_KEY=${elevenKey}`;
1408
+ fs.writeFileSync(envPath, newEnv);
1409
+ reloadEnvToProcess(hiveDir);
1410
+ log.success("✅ API key de ElevenLabs guardada en .env");
1411
+ } else {
1412
+ log.info("✅ ELEVENLABS_API_KEY ya configurada en .env");
1413
+ state.elevenlabsApiKey = elevenKey;
1414
+ }
1415
+ ttsApiKeyValue = elevenKey;
1416
+ } else if (isOpenAI) {
1417
+ const existingOpenAiKey = getEnvApiKey("OPENAI_API_KEY");
1418
+ let openaiKey: string | null = existingOpenAiKey;
1419
+
1420
+ if (!openaiKey) {
1421
+ const openaiKeyInput = await p.password({
1422
+ message: "Ingresa tu OPENAI_API_KEY:",
1423
+ validate: (value) => {
1424
+ if (!value || value.length < 10) return "API key inválida";
1425
+ },
1426
+ });
1427
+ if (p.isCancel(openaiKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1428
+ openaiKey = openaiKeyInput as string;
1429
+
1430
+ // Save to .env
1431
+ const hiveDir = getHiveDir();
1432
+ const envPath = path.join(hiveDir, ".env");
1433
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1434
+ const newEnv = envContent.includes("OPENAI_API_KEY")
1435
+ ? envContent.replace(/OPENAI_API_KEY=.*/g, `OPENAI_API_KEY=${openaiKey}`)
1436
+ : envContent + `\nOPENAI_API_KEY=${openaiKey}`;
1437
+ fs.writeFileSync(envPath, newEnv);
1438
+ reloadEnvToProcess(hiveDir);
1439
+ log.success("✅ API key de OpenAI guardada en .env");
1440
+ } else {
1441
+ log.info("✅ OPENAI_API_KEY ya configurada en .env");
1442
+ }
1443
+ ttsApiKeyValue = openaiKey;
1444
+ } else if (isGemini) {
1445
+ const existingGeminiKey = getEnvApiKey("GEMINI_API_KEY");
1446
+ let geminiKey: string | null = existingGeminiKey;
1447
+
1448
+ if (!geminiKey) {
1449
+ const geminiKeyInput = await p.password({
1450
+ message: "Ingresa tu GEMINI_API_KEY:",
1451
+ validate: (value) => {
1452
+ if (!value || value.length < 10) return "API key inválida";
1453
+ },
1454
+ });
1455
+ if (p.isCancel(geminiKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1456
+ geminiKey = geminiKeyInput as string;
1457
+
1458
+ // Save to .env
1459
+ const hiveDir = getHiveDir();
1460
+ const envPath = path.join(hiveDir, ".env");
1461
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1462
+ const newEnv = envContent.includes("GEMINI_API_KEY")
1463
+ ? envContent.replace(/GEMINI_API_KEY=.*/g, `GEMINI_API_KEY=${geminiKey}`)
1464
+ : envContent + `\nGEMINI_API_KEY=${geminiKey}`;
1465
+ fs.writeFileSync(envPath, newEnv);
1466
+ reloadEnvToProcess(hiveDir);
1467
+ log.success("✅ API key de Gemini guardada en .env");
1468
+ } else {
1469
+ log.info("✅ GEMINI_API_KEY ya configurada en .env");
1470
+ }
1471
+ ttsApiKeyValue = geminiKey;
1472
+ } else if (isQwen) {
1473
+ const existingQwenKey = getEnvApiKey("DASHSCOPE_API_KEY");
1474
+ let qwenKey: string | null = existingQwenKey;
1475
+
1476
+ if (!qwenKey) {
1477
+ const qwenKeyInput = await p.password({
1478
+ message: "Ingresa tu DASHSCOPE_API_KEY (Qwen/Alibaba):",
1479
+ validate: (value) => {
1480
+ if (!value || value.length < 10) return "API key inválida";
1481
+ },
1482
+ });
1483
+ if (p.isCancel(qwenKeyInput)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1484
+ qwenKey = qwenKeyInput as string;
1485
+
1486
+ // Save to .env
1487
+ const hiveDir = getHiveDir();
1488
+ const envPath = path.join(hiveDir, ".env");
1489
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
1490
+ const newEnv = envContent.includes("DASHSCOPE_API_KEY")
1491
+ ? envContent.replace(/DASHSCOPE_API_KEY=.*/g, `DASHSCOPE_API_KEY=${qwenKey}`)
1492
+ : envContent + `\nDASHSCOPE_API_KEY=${qwenKey}`;
1493
+ fs.writeFileSync(envPath, newEnv);
1494
+ reloadEnvToProcess(hiveDir);
1495
+ log.success("✅ API key de Qwen guardada en .env");
1496
+ } else {
1497
+ log.info("✅ DASHSCOPE_API_KEY ya configurada en .env");
1498
+ }
1499
+ ttsApiKeyValue = qwenKey;
1500
+ }
1501
+
1502
+ // Save voice config to channel (for webchat as default)
1503
+ // API keys are saved in .env and encrypted in BD
1504
+ await saveVoiceConfig({
1505
+ userId: state.userId,
1506
+ channelId: "webchat",
1507
+ voiceEnabled: true,
1508
+ sttProvider: sttProvider as string,
1509
+ ttsProvider: ttsProvider as string,
1510
+ sttApiKey: sttApiKeyValue || undefined,
1511
+ ttsApiKey: ttsApiKeyValue || undefined,
1512
+ });
1513
+ log.success("✅ Configuración de voz guardada en BD");
1514
+ }
1515
+
1516
+ const nav2 = await askNavigation();
1517
+ if (nav2 === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1518
+ if (nav2 === "prev") { step = 4; break; }
1519
+ step = 6;
1520
+ break;
1521
+ }
1522
+
1523
+ // ═══════════════════════════════════
1524
+ // STEP 6: Channels
1525
+ // ═══════════════════════════════════
1526
+ case 6: {
1527
+ showProgress(step, TOTAL_STEPS, "Canales");
1528
+
1529
+ // WebChat siempre activado por defecto
1530
+ state.channel = "webchat";
1531
+
1532
+ // ✅ Activar WebChat en BD (UPDATE del seed)
1533
+ await activateChannel(state.userId, {
1534
+ channelId: "webchat",
1535
+ channelUserId: state.sessionToken,
1536
+ });
1537
+
1538
+ log.success("✅ WebChat activado por defecto (http://localhost:18790/ui)");
1539
+
1540
+ // Preguntar si quiere activar Telegram (recomendado)
1541
+ const activateTelegram = await p.confirm({
1542
+ message: "¿Quieres activar Telegram? (recomendado para notificaciones)",
1543
+ initialValue: false,
1544
+ });
1545
+ if (p.isCancel(activateTelegram)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1546
+
1547
+ if (activateTelegram) {
1548
+ p.note(
1549
+ "1. Abre Telegram y busca @BotFather\n" +
1550
+ "2. Escribe /newbot y sigue las instrucciones\n" +
1551
+ "3. Copia el token",
1552
+ "Cómo obtener token de Telegram"
1553
+ );
1554
+ const tokenResult = await p.password({
1555
+ message: "Token de Telegram:",
1556
+ validate: (v) => (!v?.trim() ? "El token no puede estar vacío" : undefined),
1557
+ });
1558
+ if (p.isCancel(tokenResult)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1559
+ state.channelToken = tokenResult;
1560
+
1561
+ // ✅ Activar Telegram en BD (UPDATE del seed)
1562
+ await activateChannel(state.userId, {
1563
+ channelId: "telegram",
1564
+ config: { botToken: state.channelToken, dmPolicy: "open" },
1565
+ });
1566
+
1567
+ log.success("✅ Telegram activado");
1568
+ }
1569
+
1570
+ saveOnboardingProgress({
1571
+ step: "channel",
1572
+ userId: state.userId,
1573
+ data: { channel: "webchat", telegram: activateTelegram },
1574
+ });
1575
+
1576
+ const nav = await askNavigation();
1577
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1578
+ if (nav === "prev") { step = 4; break; }
1579
+ step = 7;
1580
+ break;
1581
+ }
1582
+
1583
+ // ═══════════════════════════════════
1584
+ // STEP 7: Code Bridge
1585
+ // ═══════════════════════════════════
1586
+ case 7: {
1587
+ showProgress(step, TOTAL_STEPS, "Code Bridge");
1588
+
1589
+ const codeBridgeEnabled = await p.confirm({
1590
+ message: "¿Quieres delegar tareas de código a CLIs externos? (claude-code, gemini, qwen, opencode)",
1591
+ initialValue: state.codeBridgeEnabled,
1592
+ });
1593
+ if (p.isCancel(codeBridgeEnabled)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1594
+ state.codeBridgeEnabled = codeBridgeEnabled as boolean;
1595
+
1596
+ let codeBridgeClis: string[] = [];
1597
+ if (state.codeBridgeEnabled) {
1598
+ const availableClis = getAllCodeBridge();
1599
+
1600
+ if (availableClis.length === 0) {
1601
+ log.warn("⚠️ No hay CLIs de code bridge configurados. Puedes agregar más desde el dashboard.");
1602
+ } else {
1603
+ log.info("Selecciona los CLIs que tengas instalados (presiona Espacio para seleccionar, Enter para continuar):");
1604
+ const selectedClis = await p.multiselect({
1605
+ message: "¿Qué CLIs tienes instalados?",
1606
+ options: availableClis.map(cb => ({
1607
+ value: cb.id,
1608
+ label: cb.name,
1609
+ hint: cb.cliCommand,
1610
+ })),
1611
+ required: false,
1612
+ });
1613
+ if (p.isCancel(selectedClis)) { p.cancel("Onboarding cancelado."); process.exit(0); }
1614
+ codeBridgeClis = (selectedClis as string[]) || [];
1615
+ }
1616
+ }
1617
+ state.codeBridgeClis = codeBridgeClis;
1618
+
1619
+ // ✅ Save code bridge config in DB
1620
+ const codeBridgeConfig = getAllCodeBridge().map(cb => ({
1621
+ id: cb.id,
1622
+ enabled: codeBridgeClis.includes(cb.id),
1623
+ port: cb.port,
1624
+ }));
1625
+ activateCodeBridge(state.userId, codeBridgeConfig);
1626
+
1627
+ // ✅ MCP servers se agregan desactivados (se configuran desde el dashboard)
1628
+ log.info("ℹ️ MCP servers disponibles (configura desde el dashboard)");
1629
+
1630
+ saveOnboardingProgress({
1631
+ step: "codebridge",
1632
+ userId: state.userId,
1633
+ data: { enabled: state.codeBridgeEnabled, clis: codeBridgeClis },
1634
+ });
1635
+
1636
+ const nav = await askNavigation();
1637
+ if (nav === "cancel") { p.cancel("Onboarding cancelado."); process.exit(0); }
1638
+ if (nav === "prev") { step = 5; break; }
1639
+ step = 8;
1640
+ break;
1641
+ }
1642
+
1643
+ // ═══════════════════════════════════
1644
+ // STEP 8: Resumen Final
1645
+ // ═══════════════════════════════════
1646
+ case 8: {
1647
+ showProgress(step, TOTAL_STEPS, "Resumen final");
1648
+
1649
+
1650
+ // Show summary
1651
+ p.note(
1652
+ ` Agente: ${state.agentName}\n` +
1653
+ ` Usuario: ${state.userName}\n` +
1654
+ ` Idioma: ${state.userLanguage}\n` +
1655
+ ` Proveedor: ${state.provider} (${state.model})\n` +
1656
+ ` Voz: ${state.voiceEnabled ? `STT: ${state.sttProvider}, TTS: ${state.ttsProvider}` : "no"}\n` +
1657
+ ` Canal: ${state.channel}${state.channelToken ? ' (Telegram configurado)' : ''}\n` +
1658
+ ` Code Bridge: ${state.codeBridgeEnabled ? state.codeBridgeClis.join(", ") : "no"}`,
1659
+ "📋 Resumen final"
1660
+ );
1661
+
1662
+ const confirm = await p.select({
1663
+ message: "¿Confirmar configuración?",
1664
+ options: [
1665
+ { value: "confirm", label: "✅ Confirmar", hint: "Crear agente" },
1666
+ { value: "prev", label: "⬅️ Atrás", hint: "Editar" },
1667
+ { value: "cancel", label: "❌ Cancelar", hint: "Salir" },
1668
+ ],
1669
+ });
1670
+
1671
+ if (p.isCancel(confirm) || confirm === "cancel") {
1672
+ p.cancel("Onboarding cancelado.");
1673
+ process.exit(0);
1674
+ }
1675
+
1676
+ if (confirm === "prev") {
1677
+ step = 7;
1678
+ break;
1679
+ }
1680
+
1681
+ // ✅ Create agent in DB (FINAL STEP)
1682
+ saveAgentConfig({
1683
+ userId: state.userId,
1684
+ agentId: state.agentId,
1685
+ agentName: state.agentName,
1686
+ providerId: state.provider,
1687
+ modelId: state.model,
1688
+ tone: state.agentTone,
1689
+ description: state.agentDescription,
1690
+ });
1691
+
1692
+ saveOnboardingProgress({
1693
+ step: "agent",
1694
+ userId: state.userId,
1695
+ data: { agentId: state.agentId, agentName: state.agentName },
1696
+ });
1697
+
1698
+ // 📝 Guardar IDs en .env
1699
+ const envPath = path.join(hiveDir, ".env");
1700
+ let envContent = "";
1701
+ if (fs.existsSync(envPath)) {
1702
+ envContent = fs.readFileSync(envPath, "utf-8");
1703
+ }
1704
+
1705
+ const envVars = {
1706
+ HIVE_USER_ID: state.userId,
1707
+ HIVE_AGENT_ID: state.agentId,
1708
+ };
1709
+
1710
+ let newEnvContent = envContent;
1711
+ for (const [key, value] of Object.entries(envVars)) {
1712
+ const regex = new RegExp(`^${key}=.*`, "m");
1713
+ if (regex.test(newEnvContent)) {
1714
+ newEnvContent = newEnvContent.replace(regex, `${key}=${value}`);
1715
+ } else {
1716
+ newEnvContent += `\n${key}=${value}`;
1717
+ }
1718
+ }
1719
+ fs.writeFileSync(envPath, newEnvContent.trim() + "\n");
1720
+ log.info(`IDs de identidad persistidos en ${envPath}`);
1721
+
1722
+ step = TOTAL_STEPS + 1;
1723
+ break;
1724
+ }
1725
+ }
1726
+ }
1727
+
1728
+ // ═══════════════════════════════════
1729
+ // FINAL: Show success message
1730
+ // ═══════════════════════════════════
1731
+
1732
+ p.outro(
1733
+ `✅ ¡Configuración completada!\n\n` +
1734
+ ` 🤖 Agente: ${state.agentName}\n` +
1735
+ ` 👤 Usuario: ${state.userName}\n` +
1736
+ ` 🧠 Provider: ${state.provider}/${state.model}\n` +
1737
+ ` 📢 Canal: WebChat (UI web)\n` +
1738
+ `${state.channelToken ? ` ✈️ Telegram: Configurado\n` : ''}` +
1739
+ `${state.voiceEnabled ? ` 🎤 Voz: STT (${state.sttProvider}) + TTS (${state.ttsProvider})\n` : ''}` +
1740
+ `${state.codeBridgeEnabled ? ` 🔧 Code Bridge: ${state.codeBridgeClis.join(', ')}\n` : ''}` +
1741
+ `\n` +
1742
+ `🌐 ABRE TU DASHBOARD:\n` +
1743
+ ` 👉 http://localhost:5173\n\n` +
1744
+ `📋 COMANDOS:\n` +
1745
+ ` hive start → Arrancar gateway\n` +
1746
+ ` hive chat → Chatear en terminal\n\n` +
1747
+ `💡 DESDE EL DASHBOARD puedes activar:\n` +
1748
+ ` • Canales adicionales (Discord, Slack, WhatsApp, etc.)\n` +
1749
+ ` • Voz (STT/TTS) para audio en tiempo real\n` +
1750
+ ` • Code Bridge para delegar tareas de código\n` +
1751
+ ` • MCP servers para herramientas externas\n` +
1752
+ ` • Tools y funciones personalizadas\n` +
1753
+ `\n` +
1754
+ `🎉 ¡Gracias por configurar Hive!`
1755
+ );
1756
+
1757
+ // Ask if user wants to start the gateway
1758
+ const shouldStart = await p.confirm({
1759
+ message: "¿Quieres iniciar el gateway ahora?",
1760
+ initialValue: true,
1761
+ });
1762
+
1763
+ if (p.isCancel(shouldStart) || !shouldStart) {
1764
+ log.info("¡Perfecto! Ejecuta 'hive start' cuando quieras iniciar.");
1765
+ return;
1766
+ }
1767
+
1768
+ log.info("🚀 Iniciando Hive gateway...");
1769
+ }
1770
+
1771
+ export async function onboard(): Promise<void> {
1772
+ const hiveDir = getHiveDir();
1773
+
1774
+ // Validate Hive directory existence
1775
+ if (!fs.existsSync(hiveDir)) {
1776
+ log.error(`❌ La carpeta de configuración no existe: ${hiveDir}`);
1777
+ log.info("Por favor, asegúrate de que la carpeta esté creada antes de iniciar.");
1778
+ process.exit(1);
1779
+ }
1780
+
1781
+ // Initialize DB first to check existing configuration
1782
+ initOnboardingDb();
1783
+
1784
+ // Check if there's already an agent in the database
1785
+ const { getDb } = await import("../../../core/src/storage/sqlite");
1786
+ const db = getDb();
1787
+ // We look for the coordinator agent. In a personal gateway, there is usually only one.
1788
+ const existingAgent = db.query("SELECT id, name, provider_id, model_id, status FROM agents WHERE is_coordinator = 1 ORDER BY created_at DESC LIMIT 1").get() as any;
1789
+
1790
+ // If agent exists and is completed (status = 'idle'), don't show onboarding menu
1791
+ if (existingAgent && existingAgent.status === 'idle') {
1792
+ p.intro("✅ Configuración completada detectada");
1793
+
1794
+ p.note(
1795
+ ` Agente: ${existingAgent.name}\n` +
1796
+ ` ID: ${existingAgent.id}\n` +
1797
+ ` Provider: ${existingAgent.provider_id || "no configurado"}\n` +
1798
+ ` Model: ${existingAgent.model_id || "no configurado"}\n` +
1799
+ ` Status: idle (listo para usar)`,
1800
+ "Config actual en BD"
1801
+ );
1802
+
1803
+ console.log("\n💡 Para iniciar el gateway: hive start\n");
1804
+ return;
1805
+ }
1806
+
1807
+ if (existingAgent) {
1808
+ p.intro("🔍 Configuración existente detectada");
1809
+
1810
+ p.note(
1811
+ ` Agente: ${existingAgent.name}\n` +
1812
+ ` ID: ${existingAgent.id}\n` +
1813
+ ` Provider: ${existingAgent.provider_id || "no configurado"}\n` +
1814
+ ` Model: ${existingAgent.model_id || "no configurado"}\n` +
1815
+ ` Status: ${existingAgent.status}`,
1816
+ "Config actual en BD"
1817
+ );
1818
+
1819
+ const action = await p.select({
1820
+ message: "¿Qué quieres hacer?",
1821
+ options: [
1822
+ { value: "update", label: "Actualizar", hint: "Modificar configuración" },
1823
+ { value: "reset", label: "Reiniciar", hint: "Borrar BD y crear desde cero" },
1824
+ { value: "cancel", label: "Cancelar", hint: "Salir sin cambios" },
1825
+ ],
1826
+ });
1827
+
1828
+ if (p.isCancel(action) || action === "cancel") {
1829
+ p.cancel("Operación cancelada.");
1830
+ process.exit(0);
1831
+ }
1832
+
1833
+ if (action === "reset") {
1834
+ const confirm = await p.confirm({
1835
+ message: "⚠️ Esto BORRARÁ toda la base de datos y reiniciará la configuración. ¿Continuar?",
1836
+ initialValue: false,
1837
+ });
1838
+
1839
+ if (p.isCancel(confirm) || !confirm) {
1840
+ p.cancel("Operación cancelada.");
1841
+ process.exit(0);
1842
+ }
1843
+
1844
+ // Backup and delete database
1845
+ const dbPath = path.join(hiveDir, "data", "hive.db");
1846
+ if (fs.existsSync(dbPath)) {
1847
+ const backupPath = path.join(hiveDir, `hive.db.backup.${Date.now()}`);
1848
+ fs.copyFileSync(dbPath, backupPath);
1849
+ log.info(`Backup BD creado: ${backupPath}`);
1850
+
1851
+ // Delete the database file
1852
+ fs.unlinkSync(dbPath);
1853
+ log.info("Base de datos eliminada");
1854
+ }
1855
+
1856
+ // Reinitialize DB (creates fresh with seed)
1857
+ initOnboardingDb();
1858
+
1859
+ log.success("BD reiniciada con datos del sistema");
1860
+ }
1861
+
1862
+ // After reset or update, run the wizard
1863
+ await runFullWizard();
1864
+ } else {
1865
+ // No existing configuration - run full wizard
1866
+ await runFullWizard();
1867
+ }
1868
+ }