@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,1087 @@
1
+ import { logger } from "../utils/logger.ts";
2
+ import { getDb, initializeDatabase } from "./sqlite";
3
+ import { encryptApiKey, encryptConfig, decryptApiKey, decryptConfig } from "./crypto";
4
+ import { seedAllData } from "./seed";
5
+
6
+ export interface OnboardingSection {
7
+ step: "user" | "skills" | "ethics" | "tools" | "provider" | "model" | "channel" | "codebridge" | "mcp" | "agent" | "complete";
8
+ userId: string;
9
+ data: Record<string, unknown>;
10
+ completedAt?: number;
11
+ }
12
+
13
+ const log = logger.child("onboarding");
14
+
15
+ export function initOnboardingDb(): void {
16
+ try {
17
+ initializeDatabase();
18
+
19
+ // Verificar si la DB ya tiene datos antes de hacer seed
20
+ const db = getDb();
21
+ const userCount = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
22
+
23
+ if (userCount.count > 0) {
24
+ log.info("✅ DB ya inicializada con " + userCount.count + " usuario(s). Saltando seed.");
25
+ return;
26
+ }
27
+
28
+ log.info("🌱 Ejecutando seed de datos...");
29
+ seedAllData();
30
+ log.info("✅ Seed completado correctamente.");
31
+ } catch (e) {
32
+ log.error("⚠️ Fallo al inicializar/poblar la DB:", { error: (e as Error).message });
33
+ }
34
+ }
35
+
36
+ export function saveUserProfile(data: {
37
+ userId?: string;
38
+ userName?: string;
39
+ userLanguage?: string;
40
+ userTimezone?: string;
41
+ userOccupation?: string;
42
+ userNotes?: string;
43
+ agentName?: string;
44
+ agentId?: string;
45
+ agentDescription?: string;
46
+ agentTone?: string;
47
+ channelUserId?: string;
48
+ }): string {
49
+ try {
50
+ const db = getDb();
51
+ let finalUserId = data.userId;
52
+
53
+ if (!finalUserId) {
54
+ // 1️⃣ Dejar que SQLite genere el ID automáticamente con randomblob(16)
55
+ const result = db.query(`
56
+ INSERT INTO users (name, language, timezone, occupation, notes)
57
+ VALUES (?, ?, ?, ?, ?) RETURNING id
58
+ `).get(
59
+ data.userName || null,
60
+ data.userLanguage || null,
61
+ data.userTimezone || null,
62
+ data.userOccupation || null,
63
+ data.userNotes || null
64
+ ) as { id: string };
65
+ finalUserId = result.id;
66
+ log.info("✅ User created with auto-generated ID", { userId: finalUserId });
67
+ } else {
68
+ // 1️⃣ Upsert con ID explícito (flujo web o actualización)
69
+ db.query(`
70
+ INSERT INTO users (id, name, language, timezone, occupation, notes)
71
+ VALUES (?, ?, ?, ?, ?, ?)
72
+ ON CONFLICT(id) DO UPDATE SET
73
+ name = COALESCE(excluded.name, name),
74
+ language = COALESCE(excluded.language, language),
75
+ timezone = COALESCE(excluded.timezone, timezone),
76
+ occupation = COALESCE(excluded.occupation, occupation),
77
+ notes = COALESCE(excluded.notes, notes)
78
+ `).run(
79
+ finalUserId,
80
+ data.userName || null,
81
+ data.userLanguage || null,
82
+ data.userTimezone || null,
83
+ data.userOccupation || null,
84
+ data.userNotes || null
85
+ );
86
+ }
87
+
88
+ // 2️⃣ Crear identidad base para webchat (sesión única)
89
+ if (data.channelUserId) {
90
+ db.query(`
91
+ INSERT OR REPLACE INTO user_identities (user_id, channel, channel_user_id)
92
+ VALUES (?, 'webchat', ?)
93
+ `).run(finalUserId, data.channelUserId);
94
+ log.info("✅ User identity created for webchat", { userId: finalUserId });
95
+ }
96
+
97
+ // 3️⃣ Crear o actualizar agente
98
+ if (data.agentId && data.agentName) {
99
+ const systemPrompt = buildAgentSystemPrompt(data.agentName, data.agentDescription, data.agentTone || "friendly")
100
+ db.query(`
101
+ INSERT INTO agents
102
+ (id, user_id, name, description, tone, system_prompt, status, is_coordinator)
103
+ VALUES (?, ?, ?, ?, ?, ?, 'idle', 1)
104
+ ON CONFLICT(id) DO UPDATE SET
105
+ user_id = COALESCE(excluded.user_id, user_id),
106
+ name = COALESCE(excluded.name, name),
107
+ description = COALESCE(excluded.description, description),
108
+ tone = COALESCE(excluded.tone, tone),
109
+ system_prompt = excluded.system_prompt
110
+ `).run(
111
+ data.agentId,
112
+ finalUserId,
113
+ data.agentName,
114
+ data.agentDescription || null,
115
+ data.agentTone || null,
116
+ systemPrompt
117
+ );
118
+ }
119
+
120
+ return finalUserId;
121
+ } catch (e) {
122
+ log.error("⚠️ Error saving user profile:", { error: (e as Error).message });
123
+ throw e;
124
+ }
125
+ }
126
+
127
+ export function activateSkills(userId: string, skillIds: string[]): void {
128
+ try {
129
+ const db = getDb();
130
+ // Activar skills seleccionadas
131
+ for (const skillId of skillIds) {
132
+ db.query(`UPDATE skills SET active = 1, enabled = 1 WHERE id = ?`).run(skillId);
133
+ }
134
+ log.info("✅ Skills activadas:", { skillIds: skillIds.join(", ") });
135
+ } catch (e) {
136
+ log.error("⚠️ Error activating skills:", { error: (e as Error).message });
137
+ }
138
+ }
139
+
140
+ export function activateEthics(userId: string, ethicsId: string): void {
141
+ try {
142
+ const db = getDb();
143
+ // Activar el ethics seleccionado
144
+ db.query(`UPDATE ethics SET active = 1 WHERE id = ?`).run(ethicsId);
145
+ // Desactivar los demás
146
+ db.query(`UPDATE ethics SET active = 0 WHERE id != ?`).run(ethicsId);
147
+ log.info("✅ Ethics activado:", { ethicsId });
148
+ } catch (e) {
149
+ log.error("⚠️ Error activating ethics:", { error: (e as Error).message });
150
+ }
151
+ }
152
+
153
+ export function activateTools(userId: string, toolIds: string[]): void {
154
+ try {
155
+ const db = getDb();
156
+ // Activar tools seleccionadas
157
+ for (const toolId of toolIds) {
158
+ db.query(`UPDATE tools SET active = 1, enabled = 1 WHERE id = ?`).run(toolId);
159
+ }
160
+ log.info("✅ Tools activadas:", { toolIds: toolIds.join(", ") });
161
+ } catch (e) {
162
+ log.error("⚠️ Error activating tools:", { error: (e as Error).message });
163
+ }
164
+ }
165
+
166
+ export async function saveProviderConfig(data: {
167
+ userId: string;
168
+ provider: string;
169
+ model: string;
170
+ apiKey?: string;
171
+ baseUrl?: string;
172
+ }): Promise<void> {
173
+ try {
174
+ const db = getDb();
175
+
176
+ let apiKeyEncrypted = null;
177
+ let apiKeyIv = null;
178
+
179
+ if (data.apiKey) {
180
+ const encrypted = await encryptApiKey(data.apiKey);
181
+ apiKeyEncrypted = encrypted.encrypted;
182
+ apiKeyIv = encrypted.iv;
183
+ }
184
+
185
+ // 1️⃣ Primero: Actualizar provider global con API key del usuario
186
+ db.query(`
187
+ UPDATE providers SET
188
+ api_key_encrypted = ?,
189
+ api_key_iv = ?,
190
+ base_url = ?,
191
+ enabled = 1,
192
+ active = 1
193
+ WHERE id = ?
194
+ `).run(apiKeyEncrypted, apiKeyIv, data.baseUrl || null, data.provider);
195
+
196
+ log.info("✅ Provider actualizado:", { provider: data.provider });
197
+
198
+ // 2️⃣ Segundo: Activar el modelo seleccionado
199
+ db.query(`
200
+ UPDATE models SET enabled = 1, active = 1
201
+ WHERE id = ?
202
+ `).run(data.model);
203
+
204
+ log.info("✅ Model activado:", { model: data.model });
205
+ } catch (e) {
206
+ log.error("⚠️ Error saving provider:", { error: (e as Error).message });
207
+ throw e;
208
+ }
209
+ }
210
+
211
+ export function activateCodeBridge(userId: string, codeBridgeConfig: { id: string; enabled: boolean; port?: number }[]): void {
212
+ try {
213
+ const db = getDb();
214
+ // 7️⃣ Séptimo: Configurar Code Bridge CLIs seleccionados
215
+ for (const cb of codeBridgeConfig) {
216
+ db.query(`
217
+ UPDATE code_bridge SET enabled = ?, active = ?, port = ?, user_id = ?
218
+ WHERE id = ?
219
+ `).run(cb.enabled ? 1 : 0, cb.enabled ? 1 : 0, cb.port || 18791, userId, cb.id);
220
+ }
221
+ log.info("✅ Code Bridge configurado:", { codeBridgeIds: codeBridgeConfig.map(c => c.id).join(", ") });
222
+ } catch (e) {
223
+ log.error("⚠️ Error configuring code bridge:", { error: (e as Error).message });
224
+ }
225
+ }
226
+
227
+ export function activateMcpServers(userId: string, mcpIds: string[]): void {
228
+ try {
229
+ const db = getDb();
230
+ // Activar MCP servers seleccionados
231
+ for (const mcpId of mcpIds) {
232
+ db.query(`UPDATE mcp_servers SET active = 1, enabled = 1 WHERE id = ?`).run(mcpId);
233
+ }
234
+ log.info("✅ MCP servers activados:", { mcpIds: mcpIds.join(", ") });
235
+ } catch (e) {
236
+ log.error("⚠️ Error activating MCP servers:", { error: (e as Error).message });
237
+ }
238
+ }
239
+
240
+ function buildAgentSystemPrompt(name: string, description: string | undefined, tone: string): string {
241
+ const toneGuide: Record<string, string> = {
242
+ friendly: "Sos cálido, cercano y empático. Usás un lenguaje natural y amigable, como si hablaras con un amigo de confianza.",
243
+ professional: "Sos preciso, claro y formal. Priorizás la exactitud técnica y el lenguaje estructurado.",
244
+ direct: "Sos conciso y al punto. Sin rodeos ni relleno — respondés exactamente lo que se pregunta.",
245
+ casual: "Sos relajado e informal. Podés usar humor moderado y un tono conversacional descontracturado.",
246
+ }
247
+
248
+ const toneText = toneGuide[tone] || toneGuide.friendly
249
+
250
+ const lines = [
251
+ `Sos ${name}${description ? `, ${description}` : ""}.`,
252
+ "",
253
+ `TONO Y ESTILO:`,
254
+ toneText,
255
+ "",
256
+ `PRINCIPIOS:`,
257
+ `- Siempre usá las herramientas disponibles antes de pedir información al usuario.`,
258
+ `- Confirmá acciones irreversibles (borrar archivos, cancelar tareas) antes de ejecutarlas.`,
259
+ `- Cuando no puedas completar algo, explicá brevemente por qué y qué alternativas hay.`,
260
+ `- Adaptá tu nivel técnico al contexto del usuario.`,
261
+ ]
262
+
263
+ return lines.join("\n")
264
+ }
265
+
266
+ export function saveAgentConfig(data: {
267
+ userId: string;
268
+ agentId?: string;
269
+ agentName: string;
270
+ providerId: string;
271
+ modelId: string;
272
+ tone: string;
273
+ description?: string;
274
+ }): string {
275
+ try {
276
+ const db = getDb();
277
+ let finalAgentId = data.agentId;
278
+
279
+ const systemPrompt = buildAgentSystemPrompt(data.agentName, data.description, data.tone)
280
+
281
+ // Si no se pasa agentId, dejar que SQLite lo genere automáticamente
282
+ if (!finalAgentId) {
283
+ const result = db.query(`
284
+ INSERT INTO agents
285
+ (user_id, name, description, tone, system_prompt, provider_id, model_id, status, is_coordinator, enabled)
286
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'idle', 1, 1)
287
+ RETURNING id
288
+ `).get(
289
+ data.userId,
290
+ data.agentName,
291
+ data.description || null,
292
+ data.tone,
293
+ systemPrompt,
294
+ data.providerId || null,
295
+ data.modelId || null
296
+ ) as { id: string };
297
+ finalAgentId = result.id;
298
+ log.info("✅ Agent created with auto-generated ID", { agentId: finalAgentId });
299
+ } else {
300
+ // INSERT or UPDATE agent (crea nuevo o actualiza existente)
301
+ db.query(`
302
+ INSERT INTO agents
303
+ (id, user_id, name, description, tone, system_prompt, provider_id, model_id, status, is_coordinator, enabled)
304
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'idle', 1, 1)
305
+ ON CONFLICT(id) DO UPDATE SET
306
+ user_id = COALESCE(excluded.user_id, user_id),
307
+ name = COALESCE(excluded.name, name),
308
+ description = COALESCE(excluded.description, description),
309
+ tone = COALESCE(excluded.tone, tone),
310
+ system_prompt = excluded.system_prompt,
311
+ provider_id = COALESCE(excluded.provider_id, provider_id),
312
+ model_id = COALESCE(excluded.model_id, model_id),
313
+ status = 'idle',
314
+ enabled = 1,
315
+ is_coordinator = 1
316
+ `).run(
317
+ data.agentId,
318
+ data.userId,
319
+ data.agentName,
320
+ data.description || null,
321
+ data.tone,
322
+ systemPrompt,
323
+ data.providerId || null,
324
+ data.modelId || null
325
+ );
326
+ }
327
+
328
+ return finalAgentId;
329
+ } catch (e) {
330
+ log.error("⚠️ Error saving agent:", { error: (e as Error).message });
331
+ throw e;
332
+ }
333
+ }
334
+
335
+ export async function activateChannel(userId: string, data: {
336
+ channelId: string;
337
+ channelUserId?: string; // For creating user_identity
338
+ config?: Record<string, unknown>;
339
+ }): Promise<void> {
340
+ try {
341
+ const db = getDb();
342
+
343
+ if (data.config && Object.keys(data.config).length > 0) {
344
+ const encrypted = await encryptConfig(data.config);
345
+ db.query(`
346
+ UPDATE channels
347
+ SET user_id = ?, active = 1, enabled = 1, status = 'connected',
348
+ config_encrypted = ?, config_iv = ?
349
+ WHERE id = ?
350
+ `).run(userId, encrypted.encrypted, encrypted.iv, data.channelId);
351
+ } else {
352
+ db.query(`
353
+ UPDATE channels
354
+ SET user_id = ?, active = 1, enabled = 1, status = 'connected'
355
+ WHERE id = ?
356
+ `).run(userId, data.channelId);
357
+ }
358
+
359
+ // Create user_identity for the channel if channelUserId provided
360
+ if (data.channelUserId) {
361
+ const channelType = data.channelId; // webchat, telegram, discord, etc.
362
+ db.query(`
363
+ INSERT OR REPLACE INTO user_identities (user_id, channel, channel_user_id)
364
+ VALUES (?, ?, ?)
365
+ `).run(userId, channelType, data.channelUserId);
366
+ log.info("✅ User identity created", { userId, channel: channelType });
367
+ }
368
+
369
+ log.info("✅ Channel activated:", { channelId: data.channelId, userId });
370
+ } catch (e) {
371
+ log.error("⚠️ Error activating channel:", { error: (e as Error).message });
372
+ }
373
+ }
374
+
375
+ export async function saveVoiceConfig(data: {
376
+ userId: string;
377
+ channelId: string;
378
+ voiceEnabled: boolean;
379
+ sttProvider: string;
380
+ ttsProvider: string;
381
+ sttApiKey?: string;
382
+ ttsApiKey?: string;
383
+ }): Promise<void> {
384
+ try {
385
+ const db = getDb();
386
+
387
+ // Activate STT and TTS models
388
+ db.query(`UPDATE models SET active = 1, enabled = 1 WHERE id = ?`).run(data.sttProvider);
389
+ db.query(`UPDATE models SET active = 1, enabled = 1 WHERE id = ?`).run(data.ttsProvider);
390
+
391
+ // Determine provider IDs based on model IDs
392
+ let sttProviderId = "";
393
+ let ttsProviderId = "";
394
+
395
+ if (data.sttProvider.startsWith("whisper") || data.sttProvider === "distil-whisper-large-v3-en") {
396
+ sttProviderId = "groq";
397
+ } else if (data.sttProvider === "whisper-1") {
398
+ sttProviderId = "openai";
399
+ }
400
+
401
+ if (data.ttsProvider.startsWith("eleven")) {
402
+ ttsProviderId = "elevenlabs";
403
+ } else if (data.ttsProvider.startsWith("tts-") || data.ttsProvider.startsWith("gpt-")) {
404
+ ttsProviderId = "openai";
405
+ } else if (data.ttsProvider.startsWith("gemini")) {
406
+ ttsProviderId = "gemini";
407
+ } else if (data.ttsProvider.startsWith("qwen")) {
408
+ ttsProviderId = "qwen";
409
+ }
410
+
411
+ // Save STT API key to provider if provided
412
+ if (data.sttApiKey && sttProviderId) {
413
+ const encrypted = await encryptApiKey(data.sttApiKey);
414
+ db.query(`
415
+ UPDATE providers SET
416
+ api_key_encrypted = ?,
417
+ api_key_iv = ?,
418
+ enabled = 1,
419
+ active = 1
420
+ WHERE id = ?
421
+ `).run(encrypted.encrypted, encrypted.iv, sttProviderId);
422
+ log.info("✅ STT API key guardada en BD (encriptada)", { provider: sttProviderId });
423
+ }
424
+
425
+ // Save TTS API key to provider if provided
426
+ if (data.ttsApiKey && ttsProviderId) {
427
+ const encrypted = await encryptApiKey(data.ttsApiKey);
428
+ db.query(`
429
+ UPDATE providers SET
430
+ api_key_encrypted = ?,
431
+ api_key_iv = ?,
432
+ enabled = 1,
433
+ active = 1
434
+ WHERE id = ?
435
+ `).run(encrypted.encrypted, encrypted.iv, ttsProviderId);
436
+ log.info("✅ TTS API key guardada en BD (encriptada)", { provider: ttsProviderId });
437
+ }
438
+
439
+ // Update channel with voice config
440
+ db.query(`
441
+ UPDATE channels
442
+ SET user_id = ?, voice_enabled = ?, stt_provider = ?, tts_provider = ?
443
+ WHERE id = ?
444
+ `).run(data.userId, data.voiceEnabled ? 1 : 0, data.sttProvider, data.ttsProvider, data.channelId);
445
+
446
+ log.info("✅ Voice config saved:", {
447
+ channelId: data.channelId,
448
+ userId: data.userId,
449
+ sttProvider: data.sttProvider,
450
+ ttsProvider: data.ttsProvider,
451
+ sttProviderId,
452
+ ttsProviderId
453
+ });
454
+ } catch (e) {
455
+ log.error("⚠️ Error saving voice config:", { error: (e as Error).message });
456
+ }
457
+ }
458
+
459
+ export async function saveMcpServer(data: {
460
+ userId: string;
461
+ name: string;
462
+ transport: string;
463
+ command?: string;
464
+ args?: string[];
465
+ env?: Record<string, string>;
466
+ url?: string;
467
+ enabled?: boolean;
468
+ }): Promise<void> {
469
+ try {
470
+ const db = getDb();
471
+
472
+ const mcpId = `${data.userId}:${data.name}`;
473
+
474
+ let envEncrypted = null;
475
+ let envIv = null;
476
+
477
+ if (data.env && Object.keys(data.env).length > 0) {
478
+ const encrypted = await encryptConfig(data.env as Record<string, unknown>);
479
+ envEncrypted = encrypted.encrypted;
480
+ envIv = encrypted.iv;
481
+ }
482
+
483
+ db.query(`
484
+ INSERT OR REPLACE INTO mcp_servers
485
+ (id, user_id, name, transport, command, args, env_encrypted, env_iv, url, enabled, builtin)
486
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
487
+ `).run(
488
+ mcpId,
489
+ data.userId,
490
+ data.name,
491
+ data.transport,
492
+ data.command || null,
493
+ JSON.stringify(data.args || []),
494
+ envEncrypted,
495
+ envIv,
496
+ data.url || null,
497
+ data.enabled ? 1 : 0
498
+ );
499
+
500
+ log.info("✅ MCP server saved:", { name: data.name });
501
+ } catch (e) {
502
+ log.error("⚠️ Error saving MCP server:", { error: (e as Error).message });
503
+ }
504
+ }
505
+
506
+ export function saveToolSelection(userId: string, tools: string[]): void {
507
+ try {
508
+ const db = getDb();
509
+
510
+ for (const tool of tools) {
511
+ // Activar la herramienta (ya existe del seed)
512
+ db.query(`
513
+ UPDATE tools SET active = 1, enabled = 1
514
+ WHERE id = ?
515
+ `).run(tool);
516
+ }
517
+
518
+ log.info("✅ Tools activadas:", { tools: tools.join(", ") });
519
+ } catch (e) {
520
+ log.error("⚠️ Error saving tools:", { error: (e as Error).message });
521
+ }
522
+ }
523
+
524
+ export function activateProvider(providerId: string): void {
525
+ try {
526
+ const db = getDb();
527
+ db.query(`
528
+ UPDATE providers SET active = 1, enabled = 1
529
+ WHERE id = ?
530
+ `).run(providerId);
531
+ log.info("✅ Provider activado:", { providerId });
532
+ } catch (e) {
533
+ log.error("⚠️ Error activating provider:", { error: (e as Error).message });
534
+ }
535
+ }
536
+
537
+ export function activateModel(modelId: string): void {
538
+ try {
539
+ const db = getDb();
540
+ db.query(`
541
+ UPDATE models SET active = 1, enabled = 1
542
+ WHERE id = ?
543
+ `).run(modelId);
544
+ log.info("✅ Model activado:", { modelId });
545
+ } catch (e) {
546
+ log.error("⚠️ Error activating model:", { error: (e as Error).message });
547
+ }
548
+ }
549
+
550
+
551
+
552
+ export function activateMcpServer(mcpName: string): void {
553
+ try {
554
+ const db = getDb();
555
+ db.query(`
556
+ UPDATE mcp_servers SET active = 1, enabled = 1
557
+ WHERE id = ?
558
+ `).run(mcpName);
559
+ log.info("✅ MCP server activado:", { mcpName });
560
+ } catch (e) {
561
+ log.error("⚠️ Error activating MCP server:", { error: (e as Error).message });
562
+ }
563
+ }
564
+
565
+ export function deactivateProvider(providerId: string): void {
566
+ try {
567
+ const db = getDb();
568
+ db.query(`
569
+ UPDATE providers SET active = 0, enabled = 0
570
+ WHERE id = ?
571
+ `).run(providerId);
572
+ log.warn("⚠️ Provider desactivado:", { providerId });
573
+ } catch (e) {
574
+ log.error("⚠️ Error deactivating provider:", { error: (e as Error).message });
575
+ }
576
+ }
577
+
578
+ export function deactivateModel(modelId: string): void {
579
+ try {
580
+ const db = getDb();
581
+ db.query(`
582
+ UPDATE models SET active = 0, enabled = 0
583
+ WHERE id = ?
584
+ `).run(modelId);
585
+ log.warn("⚠️ Model desactivado:", { modelId });
586
+ } catch (e) {
587
+ log.error("⚠️ Error deactivating model:", { error: (e as Error).message });
588
+ }
589
+ }
590
+
591
+ export function deactivateChannel(channelType: string): void {
592
+ try {
593
+ const db = getDb();
594
+ db.query(`
595
+ UPDATE channels SET active = 0, enabled = 0
596
+ WHERE id = ?
597
+ `).run(channelType);
598
+ log.warn("⚠️ Channel desactivado:", { channelType });
599
+ } catch (e) {
600
+ log.error("⚠️ Error deactivating channel:", { error: (e as Error).message });
601
+ }
602
+ }
603
+
604
+ export function deactivateMcpServer(mcpName: string): void {
605
+ try {
606
+ const db = getDb();
607
+ db.query(`
608
+ UPDATE mcp_servers SET active = 0, enabled = 0
609
+ WHERE id = ?
610
+ `).run(mcpName);
611
+ log.warn("⚠️ MCP server desactivado:", { mcpName });
612
+ } catch (e) {
613
+ log.error("⚠️ Error deactivating MCP server:", { error: (e as Error).message });
614
+ }
615
+ }
616
+
617
+ export function getAllProviders(): Array<{
618
+ id: string;
619
+ name: string;
620
+ baseUrl: string | null;
621
+ enabled: boolean;
622
+ active: boolean;
623
+ }> {
624
+ try {
625
+ const db = getDb();
626
+ const results = db.query(`
627
+ SELECT id, name, base_url, enabled, active
628
+ FROM providers
629
+ `).all() as Array<{
630
+ id: string;
631
+ name: string;
632
+ base_url: string | null;
633
+ enabled: number;
634
+ active: number;
635
+ }>;
636
+
637
+ return results.map(r => ({
638
+ id: r.id,
639
+ name: r.name,
640
+ baseUrl: r.base_url,
641
+ enabled: r.enabled === 1,
642
+ active: r.active === 1,
643
+ }));
644
+ } catch (e) {
645
+ log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
646
+ return [];
647
+ }
648
+ }
649
+
650
+ export function getAllModels(): Array<{
651
+ id: string;
652
+ name: string;
653
+ providerId: string;
654
+ contextWindow: number | null;
655
+ capabilities: string | null;
656
+ enabled: boolean;
657
+ active: boolean;
658
+ }> {
659
+ try {
660
+ const db = getDb();
661
+ const results = db.query(`
662
+ SELECT id, name, provider_id, context_window, capabilities, enabled, active
663
+ FROM models
664
+ `).all() as Array<{
665
+ id: string;
666
+ name: string;
667
+ provider_id: string;
668
+ context_window: number | null;
669
+ capabilities: string | null;
670
+ enabled: number;
671
+ active: number;
672
+ }>;
673
+
674
+ return results.map(r => ({
675
+ id: r.id,
676
+ name: r.name,
677
+ providerId: r.provider_id,
678
+ contextWindow: r.context_window,
679
+ capabilities: r.capabilities,
680
+ enabled: r.enabled === 1,
681
+ active: r.active === 1,
682
+ }));
683
+ } catch (e) {
684
+ log.error("⚠️ Error getting models:", { error: (e as Error).message });
685
+ return [];
686
+ }
687
+ }
688
+
689
+ export function getAllEthics(): Array<{
690
+ id: string;
691
+ name: string;
692
+ description: string | null;
693
+ content: string;
694
+ isDefault: boolean;
695
+ active: boolean;
696
+ }> {
697
+ try {
698
+ const db = getDb();
699
+ const results = db.query(`
700
+ SELECT id, name, description, content, is_default, active
701
+ FROM ethics
702
+ `).all() as Array<{
703
+ id: string;
704
+ name: string;
705
+ description: string | null;
706
+ content: string;
707
+ is_default: number;
708
+ active: number;
709
+ }>;
710
+
711
+ return results.map(r => ({
712
+ id: r.id,
713
+ name: r.name,
714
+ description: r.description,
715
+ content: r.content,
716
+ isDefault: r.is_default === 1,
717
+ active: r.active === 1,
718
+ }));
719
+ } catch (e) {
720
+ log.error("⚠️ Error getting ethics:", { error: (e as Error).message });
721
+ return [];
722
+ }
723
+ }
724
+
725
+ export function getAllCodeBridge(): Array<{
726
+ id: string;
727
+ name: string;
728
+ cliCommand: string;
729
+ port: number;
730
+ enabled: boolean;
731
+ active: boolean;
732
+ }> {
733
+ try {
734
+ const db = getDb();
735
+ const results = db.query(`
736
+ SELECT id, name, cli_command, port, enabled, active
737
+ FROM code_bridge
738
+ `).all() as Array<{
739
+ id: string;
740
+ name: string;
741
+ cli_command: string;
742
+ port: number;
743
+ enabled: number;
744
+ active: number;
745
+ }>;
746
+
747
+ return results.map(r => ({
748
+ id: r.id,
749
+ name: r.name,
750
+ cliCommand: r.cli_command,
751
+ port: r.port,
752
+ enabled: r.enabled === 1,
753
+ active: r.active === 1,
754
+ }));
755
+ } catch (e) {
756
+ log.error("⚠️ Error getting code bridge:", { error: (e as Error).message });
757
+ return [];
758
+ }
759
+ }
760
+
761
+ export function getAllSkills(): Array<{
762
+ id: string;
763
+ name: string;
764
+ description: string | null;
765
+ source: string;
766
+ isGlobal: boolean;
767
+ enabled: boolean;
768
+ active: boolean;
769
+ }> {
770
+ try {
771
+ const db = getDb();
772
+ const results = db.query(`
773
+ SELECT id, name, api_key_encrypted, api_key_iv, base_url, enabled
774
+ FROM providers
775
+ `).all() as Array<{
776
+ id: string;
777
+ name: string;
778
+ description: string | null;
779
+ source: string;
780
+ enabled: number;
781
+ active: number;
782
+ }>;
783
+
784
+ return results.map(r => ({
785
+ id: r.id,
786
+ name: r.name,
787
+ description: r.description,
788
+ source: r.source,
789
+ isGlobal: false,
790
+ enabled: r.enabled === 1,
791
+ active: r.active === 1,
792
+ }));
793
+ } catch (e) {
794
+ log.error("⚠️ Error getting skills:", { error: (e as Error).message });
795
+ return [];
796
+ }
797
+ }
798
+
799
+ export function getAllTools(): Array<{
800
+ id: string;
801
+ name: string;
802
+ description: string | null;
803
+ category: string | null;
804
+ enabled: boolean;
805
+ active: boolean;
806
+ }> {
807
+ try {
808
+ const db = getDb();
809
+ const results = db.query(`
810
+ SELECT id, name, description, category, enabled, active
811
+ FROM tools
812
+ `).all() as Array<{
813
+ id: string;
814
+ name: string;
815
+ description: string | null;
816
+ category: string | null;
817
+ enabled: number;
818
+ active: number;
819
+ }>;
820
+
821
+ return results.map(r => ({
822
+ id: r.id,
823
+ name: r.name,
824
+ description: r.description,
825
+ category: r.category,
826
+ enabled: r.enabled === 1,
827
+ active: r.active === 1,
828
+ }));
829
+ } catch (e) {
830
+ log.error("⚠️ Error getting tools:", { error: (e as Error).message });
831
+ return [];
832
+ }
833
+ }
834
+
835
+ export function getAllMcpServers(): Array<{
836
+ id: string;
837
+ name: string;
838
+ transport: string;
839
+ command: string | null;
840
+ args: string | null;
841
+ url: string | null;
842
+ builtin: boolean;
843
+ enabled: boolean;
844
+ active: boolean;
845
+ }> {
846
+ try {
847
+ const db = getDb();
848
+ const results = db.query(`
849
+ SELECT id, name, transport, command, args, url, builtin, enabled, active
850
+ FROM mcp_servers
851
+ `).all() as Array<{
852
+ id: string;
853
+ name: string;
854
+ transport: string;
855
+ command: string | null;
856
+ args: string | null;
857
+ url: string | null;
858
+ builtin: number;
859
+ enabled: number;
860
+ active: number;
861
+ }>;
862
+
863
+ return results.map(r => ({
864
+ id: r.id,
865
+ name: r.name,
866
+ transport: r.transport,
867
+ command: r.command,
868
+ args: r.args,
869
+ url: r.url,
870
+ builtin: r.builtin === 1,
871
+ enabled: r.enabled === 1,
872
+ active: r.active === 1,
873
+ }));
874
+ } catch (e) {
875
+ log.error("⚠️ Error getting MCP servers:", { error: (e as Error).message });
876
+ return [];
877
+ }
878
+ }
879
+
880
+ export function getAllChannels(): Array<{
881
+ id: string;
882
+ type: string;
883
+ accountId: string;
884
+ status: string;
885
+ enabled: boolean;
886
+ active: boolean;
887
+ }> {
888
+ try {
889
+ const db = getDb();
890
+ const results = db.query(`
891
+ SELECT id, type, id as account_id, status, enabled, active
892
+ FROM channels
893
+ `).all() as Array<{
894
+ id: string;
895
+ type: string;
896
+ account_id: string;
897
+ status: string;
898
+ enabled: number;
899
+ active: number;
900
+ }>;
901
+
902
+ return results.map(r => ({
903
+ id: r.id,
904
+ type: r.type,
905
+ accountId: r.id,
906
+ status: r.status,
907
+ enabled: r.enabled === 1,
908
+ active: r.active === 1,
909
+ }));
910
+ } catch (e) {
911
+ log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
912
+ return [];
913
+ }
914
+ }
915
+
916
+ export function getActiveTools(): Array<{
917
+ id: string;
918
+ name: string;
919
+ description: string | null;
920
+ category: string | null;
921
+ }> {
922
+ try {
923
+ const db = getDb();
924
+ const results = db.query(`
925
+ SELECT id, name, description, category
926
+ FROM tools WHERE active = 1
927
+ `).all() as Array<{
928
+ id: string;
929
+ name: string;
930
+ description: string | null;
931
+ category: string | null;
932
+ }>;
933
+
934
+ return results.map(r => ({
935
+ id: r.id,
936
+ name: r.name,
937
+ description: r.description,
938
+ category: r.category,
939
+ }));
940
+ } catch (e) {
941
+ log.error("⚠️ Error getting active tools:", { error: (e as Error).message });
942
+ return [];
943
+ }
944
+ }
945
+
946
+ export function getOnboardingProgress(userId: string): OnboardingSection | null {
947
+ try {
948
+ const db = getDb();
949
+ const result = db.query<{ step: string; data: string }, [string]>(
950
+ "SELECT step, data FROM onboarding_progress WHERE user_id = ? LIMIT 1"
951
+ ).get(userId);
952
+
953
+ if (result) {
954
+ return {
955
+ step: result.step as OnboardingSection["step"],
956
+ userId,
957
+ data: JSON.parse(result.data),
958
+ completedAt: Date.now(),
959
+ };
960
+ }
961
+ return null;
962
+ } catch {
963
+ return null;
964
+ }
965
+ }
966
+
967
+ export function saveOnboardingProgress(section: OnboardingSection): void {
968
+ try {
969
+ const db = getDb();
970
+ db.query(`
971
+ INSERT OR REPLACE INTO onboarding_progress (id, user_id, step, data)
972
+ VALUES (?, ?, ?, ?)
973
+ `).run(section.userId, section.userId, section.step, JSON.stringify(section.data));
974
+ } catch (e) {
975
+ log.error("⚠️ Error saving progress:", { error: (e as Error).message });
976
+ }
977
+ }
978
+
979
+ export async function getUserProviders(userId: string): Promise<Array<{
980
+ id: string;
981
+ name: string;
982
+ apiKey: string | null;
983
+ baseUrl: string | null;
984
+ enabled: boolean;
985
+ }>> {
986
+ try {
987
+ const db = getDb();
988
+ const results = db.query(`
989
+ SELECT id, name, api_key_encrypted, api_key_iv, base_url, enabled
990
+ FROM providers
991
+ `).all() as Array<{
992
+ id: string;
993
+ name: string;
994
+ api_key_encrypted: string | null;
995
+ api_key_iv: string | null;
996
+ base_url: string | null;
997
+ enabled: number;
998
+ }>;
999
+
1000
+ return Promise.all(results.map(async r => ({
1001
+ id: r.name,
1002
+ name: r.name,
1003
+ apiKey: r.api_key_encrypted && r.api_key_iv
1004
+ ? await decryptApiKey(r.api_key_encrypted, r.api_key_iv)
1005
+ : null,
1006
+ baseUrl: r.base_url,
1007
+ enabled: r.enabled === 1,
1008
+ })));
1009
+ } catch (e) {
1010
+ log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
1011
+ return [];
1012
+ }
1013
+ }
1014
+
1015
+ export async function getUserChannels(userId: string): Promise<Array<{
1016
+ id: string;
1017
+ type: string;
1018
+ accountId: string;
1019
+ config: Record<string, unknown>;
1020
+ enabled: boolean;
1021
+ }>> {
1022
+ try {
1023
+ const db = getDb();
1024
+ const results = db.query<{
1025
+ id: string;
1026
+ type: string;
1027
+ account_id: string;
1028
+ config_encrypted: string | null;
1029
+ config_iv: string | null;
1030
+ enabled: number;
1031
+ }, [string]>(`
1032
+ SELECT id, type, id as account_id, config_encrypted, config_iv, enabled
1033
+ FROM channels WHERE user_id = ?
1034
+ `).all(userId);
1035
+
1036
+ return Promise.all(results.map(async r => ({
1037
+ id: r.type,
1038
+ type: r.type,
1039
+ accountId: r.id,
1040
+ config: r.config_encrypted && r.config_iv
1041
+ ? await decryptConfig(r.config_encrypted, r.config_iv)
1042
+ : {},
1043
+ enabled: r.enabled === 1,
1044
+ })));
1045
+ } catch (e) {
1046
+ log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
1047
+ return [];
1048
+ }
1049
+ }
1050
+
1051
+ export function getUserAgents(userId: string): Array<{
1052
+ id: string;
1053
+ name: string;
1054
+ providerId: string | null;
1055
+ modelId: string | null;
1056
+ tone: string;
1057
+ }> {
1058
+ try {
1059
+ const db = getDb();
1060
+ const results = db.query<{
1061
+ id: string;
1062
+ name: string;
1063
+ provider_id: string | null;
1064
+ model_id: string | null;
1065
+ tone: string;
1066
+ }, [string]>(`
1067
+ SELECT id, name, provider_id, model_id, tone
1068
+ FROM agents WHERE user_id = ?
1069
+ `).all(userId);
1070
+
1071
+ return results.map(r => ({
1072
+ id: r.id,
1073
+ name: r.name,
1074
+ providerId: r.provider_id,
1075
+ modelId: r.model_id,
1076
+ tone: r.tone || "friendly",
1077
+ }));
1078
+ } catch (e) {
1079
+ log.error("⚠️ Error getting agents:", { error: (e as Error).message });
1080
+ return [];
1081
+ }
1082
+ }
1083
+
1084
+ export function maskApiKey(apiKey: string): string {
1085
+ if (!apiKey || apiKey.length < 8) return "••••••••";
1086
+ return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
1087
+ }