@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.
- package/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- 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
|
+
}
|